@hviana/sema 0.2.9 → 0.4.0

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 (84) hide show
  1. package/AGENTS.md +71 -5
  2. package/dist/src/derive/src/deduction.d.ts +12 -1
  3. package/dist/src/derive/src/deduction.js +5 -1
  4. package/dist/src/derive/src/index.d.ts +1 -0
  5. package/dist/src/geometry.d.ts +3 -30
  6. package/dist/src/geometry.js +330 -82
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/src/meter.d.ts +171 -0
  10. package/dist/src/meter.js +269 -0
  11. package/dist/src/mind/attention.d.ts +5 -4
  12. package/dist/src/mind/attention.js +254 -23
  13. package/dist/src/mind/bridge.d.ts +10 -1
  14. package/dist/src/mind/bridge.js +179 -10
  15. package/dist/src/mind/canonical.d.ts +6 -1
  16. package/dist/src/mind/canonical.js +6 -1
  17. package/dist/src/mind/graph-search.d.ts +9 -0
  18. package/dist/src/mind/graph-search.js +59 -19
  19. package/dist/src/mind/index.d.ts +2 -0
  20. package/dist/src/mind/junction.d.ts +10 -0
  21. package/dist/src/mind/junction.js +14 -0
  22. package/dist/src/mind/learning.d.ts +32 -4
  23. package/dist/src/mind/learning.js +26 -4
  24. package/dist/src/mind/match.d.ts +40 -0
  25. package/dist/src/mind/match.js +125 -1
  26. package/dist/src/mind/mechanisms/cast.js +63 -6
  27. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  28. package/dist/src/mind/mechanisms/extraction.js +1 -88
  29. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  30. package/dist/src/mind/mechanisms/recall.js +77 -14
  31. package/dist/src/mind/mind.d.ts +59 -5
  32. package/dist/src/mind/mind.js +115 -93
  33. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  34. package/dist/src/mind/pipeline-mechanism.js +179 -10
  35. package/dist/src/mind/pipeline.d.ts +29 -0
  36. package/dist/src/mind/pipeline.js +79 -21
  37. package/dist/src/mind/primitives.d.ts +11 -15
  38. package/dist/src/mind/primitives.js +47 -28
  39. package/dist/src/mind/reasoning.d.ts +7 -1
  40. package/dist/src/mind/reasoning.js +40 -8
  41. package/dist/src/mind/recognition.js +93 -20
  42. package/dist/src/mind/traverse.d.ts +11 -0
  43. package/dist/src/mind/traverse.js +88 -7
  44. package/dist/src/mind/types.d.ts +39 -5
  45. package/dist/src/store.d.ts +15 -0
  46. package/dist/src/store.js +91 -6
  47. package/package.json +1 -1
  48. package/src/derive/src/deduction.ts +15 -0
  49. package/src/derive/src/index.ts +1 -0
  50. package/src/geometry.ts +350 -122
  51. package/src/index.ts +1 -0
  52. package/src/meter.ts +333 -0
  53. package/src/mind/attention.ts +276 -31
  54. package/src/mind/bridge.ts +187 -10
  55. package/src/mind/canonical.ts +6 -1
  56. package/src/mind/graph-search.ts +60 -21
  57. package/src/mind/index.ts +6 -0
  58. package/src/mind/junction.ts +12 -0
  59. package/src/mind/learning.ts +46 -5
  60. package/src/mind/match.ts +146 -1
  61. package/src/mind/mechanisms/cast.ts +62 -6
  62. package/src/mind/mechanisms/extraction.ts +2 -103
  63. package/src/mind/mechanisms/recall.ts +84 -17
  64. package/src/mind/mind.ts +144 -99
  65. package/src/mind/pipeline-mechanism.ts +203 -13
  66. package/src/mind/pipeline.ts +144 -36
  67. package/src/mind/primitives.ts +49 -33
  68. package/src/mind/reasoning.ts +39 -7
  69. package/src/mind/recognition.ts +89 -19
  70. package/src/mind/traverse.ts +89 -8
  71. package/src/mind/types.ts +39 -5
  72. package/src/store.ts +75 -6
  73. package/test/14-scaling.test.mjs +17 -7
  74. package/test/31-audit.test.mjs +4 -1
  75. package/test/33-multi-candidate.test.mjs +13 -1
  76. package/test/36-already-answered-fusion.test.mjs +10 -3
  77. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  78. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  79. package/test/54-evidence-k-instrumentation.test.mjs +175 -0
  80. package/test/55-cost-meter.test.mjs +284 -0
  81. package/test/56-bridge-identity-admission.test.mjs +209 -0
  82. package/test/57-fusion-order.test.mjs +104 -0
  83. package/test/58-subquantum-sites.test.mjs +112 -0
  84. package/test/59-fold-invariance.test.mjs +226 -0
@@ -37,7 +37,12 @@ import {
37
37
  import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
38
38
  import { recognise } from "./recognition.js";
39
39
  import { leafIdRun } from "./canonical.js";
40
- import { corpusN, edgeAncestors, hubBound } from "./traverse.js";
40
+ import {
41
+ corpusN,
42
+ edgeAncestors,
43
+ hubBound,
44
+ sharedReachMemo,
45
+ } from "./traverse.js";
41
46
  import {
42
47
  cachedRead,
43
48
  type Junction,
@@ -149,6 +154,11 @@ export interface ConsensusReachTrace {
149
154
  contextsReached: number;
150
155
  saturated: boolean;
151
156
  saturation?: SaturationStop;
157
+ /** Nodes the climb processed — see {@link AncestorReach.visited}. Absent
158
+ * on payloads recorded before this field existed. */
159
+ visited?: number;
160
+ /** Maximum ascent distance — see {@link AncestorReach.maxDepth}. */
161
+ maxDepth?: number;
152
162
  }
153
163
 
154
164
  export type AnchorRejectionReason =
@@ -419,6 +429,9 @@ function serialiseReaches(
419
429
  contextsReached: r.contextsReached,
420
430
  saturated: r.saturated,
421
431
  ...(r.saturation ? { saturation: r.saturation } : {}),
432
+ ...(r.visited !== undefined
433
+ ? { visited: r.visited, maxDepth: r.maxDepth }
434
+ : {}),
422
435
  });
423
436
  }
424
437
  return out;
@@ -467,6 +480,7 @@ export async function climbAttentionAll(
467
480
  }
468
481
  const hit = byRead.get(modeKey);
469
482
  if (hit !== undefined) {
483
+ if (ctx.meter) ctx.meter.climbHits++;
470
484
  // Cache-hit exit (spec §9): the abbreviated payload shape — only what
471
485
  // is actually stored in the cached AttentionRead is reported. No
472
486
  // candidate, reach, saturation, pooling or anchor detail is fabricated
@@ -506,6 +520,7 @@ export async function computeAttention(
506
520
  k: number,
507
521
  mode: DFMode,
508
522
  ): Promise<AttentionRead> {
523
+ if (ctx.meter) ctx.meter.climbs++;
509
524
  const regions = collectRegions(ctx, query);
510
525
  const perceivedCount = regions.length;
511
526
 
@@ -523,6 +538,22 @@ export async function computeAttention(
523
538
  v: gistOf(ctx, query.subarray(s.start, s.end)),
524
539
  start: s.start,
525
540
  end: s.end,
541
+ // NOT a chunk — a precondition, not a judgement about the evidence.
542
+ // `chunk` admits a region into the saturated-INTERVAL builder (see
543
+ // crossRegionVotes), which walks regions as a SEQUENCE and merges
544
+ // neighbouring saturated ones into runs. Its own contract requires the
545
+ // regions it reads to be DISJOINT and in byte order — true of
546
+ // leaf-parents, false of sites, which overlap each other and the chunks
547
+ // ("red", "circle" and "red circle" are all present at once).
548
+ //
549
+ // Admitting them was measured both ways and is unprincipled in each
550
+ // direction: a saturated site EXTENDS a run and masks votes that should
551
+ // have won (test/37 lost all three — roots became "red " and "hat"
552
+ // instead of "red circle" and "2"), while a non-saturated one BREAKS a
553
+ // run and unmasks votes that should have been dropped, which is the only
554
+ // reason it appeared to fix test/34. Either way the outcome turns on
555
+ // where an overlapping span happens to fall in the array — the same
556
+ // positional accident this work exists to remove.
526
557
  chunk: false,
527
558
  known: true, // a recognised site IS a stored form
528
559
  });
@@ -549,25 +580,30 @@ export async function computeAttention(
549
580
  const N = corpusN(ctx);
550
581
  // One climb per distinct anchor for the WHOLE query: regions sharing a
551
582
  // chunk, and canonicalChunkId's prefix probes, all hit this memo instead of
552
- // re-reading the anchor's full edge fan-out from the store.
553
- const reachMemo = new Map<number, AncestorReach>();
554
- const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo, td);
583
+ // re-reading the anchor's full edge fan-out from the store. The memo is
584
+ // the SHARED one (traverse.ts) — response-scoped for respond(),
585
+ // conversation-scoped across turns, and the same map confluence prices
586
+ // commonality against; it used to be a private per-climb Map, so a
587
+ // conversation re-climbed its own repeated regions from cold on every
588
+ // turn. A traced response still gets a fresh one — see sharedReachMemo.
589
+ const reachMemo = sharedReachMemo(ctx);
590
+ const rvs = ctx.meter
591
+ ? await ctx.meter.time(
592
+ "climb.voteRegions",
593
+ () => voteRegions(ctx, query, regions, k, mode, N, reachMemo, td),
594
+ )
595
+ : await voteRegions(ctx, query, regions, k, mode, N, reachMemo, td);
555
596
 
556
597
  // ── Cross-region: DIRECT region-to-region interaction ─────────────────
557
598
  // Two regions whose individual climbs land on DIFFERENT contexts leave
558
599
  // their JOINT context — the learnt whole that contains BOTH — with no
559
600
  // vote. crossRegionVotes recovers it by the bridge's content-addressed
560
601
  // junction ascent (see the note above the function).
561
- const cross = await crossRegionVotes(
562
- ctx,
563
- query,
564
- regions,
565
- rvs,
566
- k,
567
- N,
568
- reachMemo,
569
- td,
570
- );
602
+ const crossArgs = () =>
603
+ crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td);
604
+ const cross = ctx.meter
605
+ ? await ctx.meter.time("climb.crossRegion", crossArgs)
606
+ : await crossArgs();
571
607
  // A vote SUPERSEDED by exact joint evidence (its bytes literally live
572
608
  // inside the joint container, yet it climbed elsewhere — grid aliasing)
573
609
  // is dropped, not down-weighted: the joint container explains it away.
@@ -627,7 +663,19 @@ export function collectRegions(ctx: MindContext, query: Uint8Array): Region[] {
627
663
  // node, the same lookups a deposit pays.
628
664
  foldTree(ctx, perceive(ctx, query), 0, (n, start, end, node) => {
629
665
  if (n.kids === null) return;
630
- if (!dominates(end - start, query.length) || regions.length === 0) {
666
+ // The dominance filter is about WRAPPERS, not about size. A chunk is the
667
+ // smallest grouped unit — it wraps no other region — so it can never be the
668
+ // "broad, non-discriminative wrapper" this rule exists to exclude, however
669
+ // much of a short query it happens to cover. Testing it by span alone was
670
+ // safe only while chunks were exactly W bytes: content-defined segments run
671
+ // up to the keyring's seat count, so on a 15-byte query the 8-byte segment
672
+ // "is frigi" counted as dominant and was discarded, leaving CAST one point
673
+ // of attention where it needs two (test/29 D1/D2). Composites are still
674
+ // filtered exactly as before.
675
+ if (
676
+ isChunk(n) || !dominates(end - start, query.length) ||
677
+ regions.length === 0
678
+ ) {
631
679
  regions.push({
632
680
  v: n.v,
633
681
  start,
@@ -636,6 +684,25 @@ export function collectRegions(ctx: MindContext, query: Uint8Array): Region[] {
636
684
  known: node !== null,
637
685
  });
638
686
  }
687
+ // MEASURED AND REFUTED — subdividing a long segment into W-scale tiles.
688
+ // A content segment runs from W−1 up to the keyring's seat count (2W) and
689
+ // folds FLAT, so its only sub-units are single bytes; the grid's regions
690
+ // were always exactly W. Offering each segment's W-byte tiles as extra
691
+ // regions (gists only, no stored nodes, anchored on the segment's own
692
+ // content-defined start so invariance is kept) does restore that finer
693
+ // grain: on `How is ice like steel?` the climb went from ONE ranked anchor
694
+ // to three, and test/33's own CAST-candidate spread was recovered.
695
+ //
696
+ // It is still wrong, and net worse (measured: test/29 went 9/2 to 7/4).
697
+ // canonicalWindows governs EXACT identity lookup, which recognition
698
+ // already probes at every offset — it says nothing about the grain of an
699
+ // approximate gist, so "the write side's unit scale" was two machineries
700
+ // conflated. What the tiles actually do is reintroduce a fixed stride
701
+ // inside the segment, and the extra votes reorder the climb: on
702
+ // `How is Shakespeare like Leonardo da Vinci?` the short name deposits
703
+ // outranked the exemplar sentences, claimed their aligned runs first, and
704
+ // left the sentences CAST needs with no free run at all (C2, C3). A
705
+ // region must come from the fold, not from a stride over it.
639
706
  });
640
707
  return regions;
641
708
  }
@@ -654,13 +721,18 @@ export async function voteRegions(
654
721
  saturated: boolean[];
655
722
  voters: Array<{ id: number; score: number; w: number } | null>;
656
723
  }> {
724
+ if (ctx.meter) ctx.meter.climbRegions += regions.length;
657
725
  const regionSaturated: boolean[] = new Array(regions.length).fill(false);
658
726
  const regionVotes: RegionVote[] = [];
659
727
  const regionVoter: Array<{ id: number; score: number; w: number } | null> =
660
728
  ctx.trace ? regions.map(() => null) : [];
661
729
 
730
+ const W = ctx.space.maxGroup;
662
731
  for (let ri = 0; ri < regions.length; ri++) {
663
- const { v, start, end, chunk, known } = regions[ri];
732
+ // `v`/`start`/`end` are rebindable: a long approximate segment may vote
733
+ // with the sub-span that actually carries its evidence — see below.
734
+ let { v, start, end } = regions[ri];
735
+ const { chunk, known } = regions[ri];
664
736
  // Trace-only bookkeeping for this region — allocated only under `td`
665
737
  // (i.e. only when ctx.trace is set); see ConsensusRegionTrace/
666
738
  // RegionOutcome (spec §4). `examinedIds` tracks distinct ANN hits
@@ -704,10 +776,10 @@ export async function voteRegions(
704
776
  // the resonate() call for most exact regions — the single largest
705
777
  // remaining inference sink — with the anchor choice unchanged (the
706
778
  // canonical branch already ignored hits[0]).
707
- const canonicalId = chunk
779
+ let canonicalId = chunk
708
780
  ? canonicalChunkId(ctx, query.subarray(start, end), N, reachMemo)
709
781
  : null;
710
- const canonicalUsable = canonicalId !== null &&
782
+ let canonicalUsable = canonicalId !== null &&
711
783
  (ctx.store.hasParents(canonicalId) ||
712
784
  ctx.store.hasContainers(canonicalId));
713
785
  let hits: readonly Hit[] | null = null;
@@ -719,6 +791,80 @@ export async function voteRegions(
719
791
  return hits;
720
792
  };
721
793
 
794
+ // A DILUTED SEGMENT VOTES WITH THE SPAN THAT CARRIES ITS EVIDENCE.
795
+ //
796
+ // A content segment runs up to the keyring's seat count and folds FLAT, so
797
+ // its gist superposes every one of its bytes: an entity inside a longer
798
+ // segment is averaged together with whatever scaffolding shares the
799
+ // segment, and the resonance reads the average. Measured on
800
+ // `How is ice like steel?` against a store holding `Steel is hard`: the
801
+ // segment `ike stee` resonates to `Ice is c` at 0.297 — the WRONG deposit —
802
+ // with `Steel ` fourth at 0.123, while the sub-span `stee` resonates to
803
+ // `Steel ` at 0.627. The evidence is there; the whole-segment read cannot
804
+ // see it, and `Steel is hard` received no vote at all (test/29 C1).
805
+ //
806
+ // Entered only after the EXACT path has already failed — a chunk with a
807
+ // usable canonical identity has a content-addressed handle on its own bytes
808
+ // and needs no estimator at all — so this is honest degradation, not extra
809
+ // work on regions that already resolved. The candidates are the segment's
810
+ // two EDGE sub-spans at the write side's own unit scale (W) — the scale
811
+ // `canonicalWindows` interns, and the only one at which a sub-span could
812
+ // carry a stored identity; a segment of W or less has no interior at all.
813
+ // Edges because a content cut lands INSIDE a unit, so the remnant it split
814
+ // sits against the cut: `steel` is cut after `stee`. Offering every
815
+ // interior offset instead was measured and is worse — it re-anchors
816
+ // segments on spans no boundary ever separated, and broke three of
817
+ // test/17's vote-distribution and root-count assertions (428/1 → 424/5).
818
+ //
819
+ // Selection is by the SAME quantity the region's vote is weighted by —
820
+ // score² · idf — never by score alone: the scaffolding window `is i`
821
+ // resonates at 0.832, far above `stee`, and is worth nothing because its
822
+ // reach is the whole corpus. Nothing new is being measured here; the
823
+ // choice the code did not previously make is made with the criterion it
824
+ // already uses. The region's SPAN narrows with its gist, so breadth,
825
+ // clusters and cross-region pairing all see where the evidence really sits.
826
+ if (!canonicalUsable && chunk && !known && end - start > W) {
827
+ const weigh = (h: Hit): { w: number; id: number } | null => {
828
+ const r = edgeAncestors(ctx, h.id, N, reachMemo);
829
+ if (r.saturated || r.roots.length === 0) return null;
830
+ const idf = Math.log(N / Math.max(1, r.contextsReached));
831
+ if (idf <= 0) return null;
832
+ return { w: h.score * h.score * idf, id: h.id };
833
+ };
834
+ // The whole-segment candidate reuses the ranking the region needs
835
+ // anyway, so only the two edge probes are new work.
836
+ const scoreOf = async (
837
+ gist: Vec,
838
+ ): Promise<{ w: number; id: number } | null> => {
839
+ const h = await ctx.store.resonate(gist, 1);
840
+ return h.length === 0 ? null : weigh(h[0]);
841
+ };
842
+ const h0 = await ensureHits();
843
+ let best = h0.length > 0 ? weigh(h0[0]) : null;
844
+ let bestSpan: [number, number, Vec] | null = null;
845
+ for (const s0 of [start, end - W]) {
846
+ const sub = gistOf(ctx, query.subarray(s0, s0 + W));
847
+ const cand = await scoreOf(sub);
848
+ if (cand !== null && (best === null || cand.w > best.w)) {
849
+ best = cand;
850
+ bestSpan = [s0, s0 + W, sub];
851
+ }
852
+ }
853
+ if (bestSpan !== null) {
854
+ [start, end, v] = bestSpan;
855
+ hits = null; // the whole-segment ranking no longer describes this span
856
+ canonicalId = canonicalChunkId(
857
+ ctx,
858
+ query.subarray(start, end),
859
+ N,
860
+ reachMemo,
861
+ );
862
+ canonicalUsable = canonicalId !== null &&
863
+ (ctx.store.hasParents(canonicalId) ||
864
+ ctx.store.hasContainers(canonicalId));
865
+ }
866
+ }
867
+
722
868
  const canonicalFailed = chunk && canonicalId === null;
723
869
  let voterId: number;
724
870
  let score: number;
@@ -1015,6 +1161,9 @@ export function poolVotes(
1015
1161
  };
1016
1162
  }
1017
1163
 
1164
+ // The one hub bound (traverse.ts) — N here IS corpusN, threaded down from
1165
+ // computeAttention. Read once, not per rule application.
1166
+ const bound = hubBound(ctx);
1018
1167
  const key = (it: AItem) =>
1019
1168
  it.kind === "region"
1020
1169
  ? `r${it.ri}`
@@ -1045,13 +1194,14 @@ export function poolVotes(
1045
1194
  // region's vote across its FULL corpus-sized fan-in yields O(corpus)
1046
1195
  // rule applications per region and near-zero per-target weight anyway.
1047
1196
  // Cap the redistribution at the first √N contexts (insertion order,
1048
- // the same convention chooseNext caps by).
1049
- const hubBound = Math.ceil(Math.sqrt(N));
1197
+ // the same convention chooseNext caps by). Hoisted out of the
1198
+ // generator: `rules` is invoked once per popped item, and this used to
1199
+ // re-derive the bound on every one of them.
1050
1200
  for (const r of rv.roots) {
1051
1201
  // CAPPED read: only the first hubBound targets are ever credited, so
1052
1202
  // only they are read — a common continuation's full reverse fan-in
1053
1203
  // is corpus-sized and is never materialised.
1054
- const pv = ctx.store.prevFirst(r, hubBound);
1204
+ const pv = ctx.store.prevFirst(r, bound);
1055
1205
  const isAnswer = pv.length > 0 && !ctx.store.hasNext(r);
1056
1206
  const targets = isAnswer ? pv : [r];
1057
1207
  for (const t of targets) {
@@ -1098,7 +1248,12 @@ export function poolVotes(
1098
1248
  const rv = regionVotes[p0.ri];
1099
1249
  breadthSum += rv.absorbed ?? 1;
1100
1250
  premises.push({ kind: "form", span: [rv.start, rv.end] });
1101
- spans.push([rv.start, rv.end]);
1251
+ // A vote knows where its own evidence sits: `parts` when it stands on
1252
+ // several separate places (a joint binding), the merged span
1253
+ // otherwise. See RegionVote.parts.
1254
+ if (rv.parts !== undefined) {
1255
+ for (const [s, e] of rv.parts) spans.push([s, e]);
1256
+ } else spans.push([rv.start, rv.end]);
1102
1257
  }
1103
1258
  regionSupport.set(pc.item.id, breadthSum);
1104
1259
  regionSpans.set(pc.item.id, spans);
@@ -1403,13 +1558,38 @@ export function canonicalChunkId(
1403
1558
  reachMemo?: Map<number, AncestorReach>,
1404
1559
  ): number | null {
1405
1560
  const len = Math.min(regionBytes.length, ctx.space.maxGroup);
1561
+ // WHICH window anchors a region is decided by reach, not by position. This
1562
+ // used to return at the FIRST offset that matched, which was indistinguishable
1563
+ // from correct while every region was exactly W bytes — there was only one
1564
+ // offset. A content-defined segment is longer, and its first window is
1565
+ // whatever happens to start it: for "is frigi" that is " is ", pure
1566
+ // scaffolding, which reaches every context, saturates, and makes the whole
1567
+ // region ABSTAIN. The region's own content ("frigi") never got a say, and
1568
+ // CAST lost a point of attention it needed (test/29 D1/D2).
1569
+ //
1570
+ // So scan every offset and prefer an anchor that still discriminates: not
1571
+ // saturated, and among those the one reaching the FEWEST contexts (§2.7,
1572
+ // corpus-global). Only when every window in the region saturates does the
1573
+ // old generalising choice stand — there is then no discriminative anchor to
1574
+ // find, and abstaining is the honest outcome.
1575
+ let discId: number | null = null;
1576
+ let discReached = Infinity;
1577
+ let fallback: number | null = null;
1406
1578
  for (let off = 0; off + len <= regionBytes.length; off++) {
1407
1579
  const ids = leafIdRun(ctx, regionBytes, off, off + len);
1408
- if (ids === null) return null;
1580
+ // An unknown byte disqualifies THIS window, not the region. This used to
1581
+ // abandon the whole region on the first unseen byte, which was
1582
+ // indistinguishable from correct while regions were exactly W bytes — there
1583
+ // was one window, so failing it was failing the region. A content-defined
1584
+ // segment holds several windows, and a single unknown byte near its start
1585
+ // was silently costing the region its anchor entirely.
1586
+ if (ids === null) continue;
1409
1587
  const flatId = ctx.store.findBranch(ids);
1410
1588
  if (flatId === null) continue;
1411
1589
  if (len < 2) return flatId;
1412
1590
 
1591
+ // Within one window, the widest reach is still the right CANONICAL
1592
+ // identity — a chunk's anchor should be its most general stable form.
1413
1593
  let bestId = flatId;
1414
1594
  let bestReach = edgeAncestors(ctx, flatId, N, reachMemo);
1415
1595
  for (let k2 = 1; k2 < len; k2++) {
@@ -1425,9 +1605,16 @@ export function canonicalChunkId(
1425
1605
  bestReach = shortReach;
1426
1606
  }
1427
1607
  }
1428
- return bestId;
1608
+ if (fallback === null) fallback = bestId;
1609
+ if (!bestReach.saturated && bestReach.contextsReached < discReached) {
1610
+ discId = bestId;
1611
+ discReached = bestReach.contextsReached;
1612
+ // Nothing can discriminate better than reaching ONE context, so the scan
1613
+ // stops there rather than pricing the rest of the segment's windows.
1614
+ if (discReached <= 1) break;
1615
+ }
1429
1616
  }
1430
- return null;
1617
+ return discId ?? fallback;
1431
1618
  }
1432
1619
 
1433
1620
  export function naturalBreak(votes: number[]): number {
@@ -1786,6 +1973,24 @@ function betterProposal(
1786
1973
  * ANN-query each, merge proposals by candidate id, and validate the winner
1787
1974
  * through the SAME structural gates every other tier answers to (saturation,
1788
1975
  * roots, IDF, contrastive margin). Returns null when nothing survives. */
1976
+ /** {@link structuralResonance}, charged to its own profiling phase — it is
1977
+ * the halo-mediated arm of the cross-region ladder and the one part of it
1978
+ * that resonates. */
1979
+ async function meteredStructuralResonance(
1980
+ ...args: Parameters<typeof structuralResonance>
1981
+ ): Promise<
1982
+ ReturnType<typeof structuralResonance> extends Promise<infer R> ? R
1983
+ : never
1984
+ > {
1985
+ const ctx = args[0];
1986
+ return ctx.meter
1987
+ ? await ctx.meter.time(
1988
+ "climb.structuralResonance",
1989
+ () => structuralResonance(...args),
1990
+ )
1991
+ : await structuralResonance(...args);
1992
+ }
1993
+
1789
1994
  export async function structuralResonance(
1790
1995
  ctx: MindContext,
1791
1996
  query: Uint8Array,
@@ -1987,7 +2192,19 @@ async function crossRegionVotes(
1987
2192
  // one side is always individually discriminative.
1988
2193
  //
1989
2194
  // Only MAXIMAL spans compose: a span contained in another candidate is a
1990
- // fragment of that candidate's evidence, never independent of it.
2195
+ // fragment of that candidate's evidence, never independent of it — but
2196
+ // containment alone does not establish that relation. An APPROXIMATE
2197
+ // container (a fold segment whose gist merely resonated) does not hold the
2198
+ // evidence of an EXACT one (a recognised site, content-addressed): its
2199
+ // bytes straddle the site rather than explain it, so calling the site a
2200
+ // fragment of it discards the only exact reading of those bytes. Measured:
2201
+ // on `blue then square` the segment `blue t` swallowed the site `blue`,
2202
+ // leaving one candidate and no pair, while on `red then circle` the
2203
+ // segment happened to end at `red `'s edge and the same query shape
2204
+ // composed — the outcome turned on where a cut fell. This is the same
2205
+ // discipline the between-region gate below already states: an approximate
2206
+ // region climbing "somewhere" is ordinary noise, not evidence.
2207
+ //
1991
2208
  // Shared across every cross-region probe in this climb: a sibling
1992
2209
  // successfully reconstructed while probing one pair must not be read and
1993
2210
  // perceived again while probing another pair in the same climb.
@@ -2011,7 +2228,8 @@ async function crossRegionVotes(
2011
2228
  y !== x &&
2012
2229
  regions[y].start <= regions[x].start &&
2013
2230
  regions[x].end <= regions[y].end &&
2014
- regions[y].end - regions[y].start > regions[x].end - regions[x].start
2231
+ regions[y].end - regions[y].start > regions[x].end - regions[x].start &&
2232
+ (regions[y].known || !regions[x].known)
2015
2233
  )
2016
2234
  );
2017
2235
  const none = { votes: [], superseded: new Set<RegionVote>() };
@@ -2277,7 +2495,7 @@ async function crossRegionVotes(
2277
2495
  const ownRootsB = rvs.votes.find((v) =>
2278
2496
  v.start === rb.start && v.end === rb.end
2279
2497
  )?.roots;
2280
- structuralPick = await structuralResonance(
2498
+ structuralPick = await meteredStructuralResonance(
2281
2499
  ctx,
2282
2500
  query,
2283
2501
  ra,
@@ -2454,9 +2672,26 @@ async function crossRegionVotes(
2454
2672
  spanStart = Math.min(spanStart, regions[ei].start);
2455
2673
  spanEnd = Math.max(spanEnd, regions[ei].end);
2456
2674
  }
2457
- consumed.add(cand[a]);
2458
- consumed.add(cand[b]);
2459
- for (const ei of bestExtras) consumed.add(ei);
2675
+ // CONSUMPTION IS FOR CONTAINER-BACKED EVIDENCE ONLY. Consuming a
2676
+ // candidate says "its evidence is already composed at full joint
2677
+ // strength, re-pairing it would vote the same container twice" — a
2678
+ // claim only a real container can make. A structural-resonance pick
2679
+ // has none (see above: it is NOT a Junction), so consuming its
2680
+ // endpoints locks up candidates on the strength of an ANN guess and
2681
+ // stops genuine evidence from ever composing them. Measured: on
2682
+ // `greet reply-greet then red then circle` the pair
2683
+ // `reply-greet` ▸ `red` resonated to `red square` and consumed `red`,
2684
+ // after which `red` ▸ `circle` was never probed and the exact junction
2685
+ // `red circle` — a stored whole, sitting right there — went unfound.
2686
+ // This is spec §15's asymmetry (only exact DAG evidence may explain
2687
+ // ordinary votes away) applied to the other way a tier can silence
2688
+ // evidence. Both votes now stand and pooling decides between them,
2689
+ // which is what the mechanism market is for.
2690
+ if (structuralPick === null) {
2691
+ consumed.add(cand[a]);
2692
+ consumed.add(cand[b]);
2693
+ for (const ei of bestExtras) consumed.add(ei);
2694
+ }
2460
2695
 
2461
2696
  // EXPLAINING AWAY — see the block comment above the function. Byte
2462
2697
  // containment in the joint container is the relatedness test (the
@@ -2506,6 +2741,16 @@ async function crossRegionVotes(
2506
2741
  w,
2507
2742
  wFocus: w,
2508
2743
  absorbed: 1 + explainedAway,
2744
+ // The places this junction actually stands on — its two endpoints and
2745
+ // any N-ary extras, NOT the merged span [spanStart, spanEnd], which
2746
+ // swallows the gap and reads as one neighbourhood. See
2747
+ // RegionVote.parts.
2748
+ parts: [cand[a], cand[b], ...bestExtras]
2749
+ .map((ri): readonly [number, number] => [
2750
+ regions[ri].start,
2751
+ regions[ri].end,
2752
+ ])
2753
+ .sort((x, y) => x[0] - y[0]),
2509
2754
  });
2510
2755
  pushProbe("accepted");
2511
2756
  if (td) {