@hviana/sema 0.2.4 → 0.2.5

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.
@@ -25,23 +25,288 @@ import type {
25
25
  Region,
26
26
  RegionVote,
27
27
  SaturationInfo,
28
+ SaturationStop,
28
29
  } from "./types.js";
29
- import { consensusFloor, dominates, estimatorNoise } from "../geometry.js";
30
+ import {
31
+ composeStructuralGist,
32
+ consensusFloor,
33
+ dominates,
34
+ estimatorNoise,
35
+ type StructuralPart,
36
+ } from "../geometry.js";
30
37
  import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
31
38
  import { recognise } from "./recognition.js";
32
39
  import { leafIdRun } from "./canonical.js";
33
- import { corpusN, edgeAncestors } from "./traverse.js";
40
+ import { corpusN, edgeAncestors, hubBound } from "./traverse.js";
34
41
  import {
35
42
  cachedRead,
43
+ type Junction,
36
44
  junctionContainersFrom,
37
45
  junctionSeeds,
38
46
  junctionSynonyms,
47
+ type JunctionSynonymSides,
48
+ loadJunctionSynonymSides,
49
+ type SynonymJunction,
39
50
  walkCache,
40
51
  } from "./junction.js";
52
+ import type { Vec } from "../vec.js";
41
53
  import { indexOf } from "../bytes.js";
42
54
  import type { RationaleItem } from "./rationale.js";
43
55
  import { rDeriv, rItem, rNode, traceDerivation } from "./trace.js";
44
56
 
57
+ // ═══════════════════════════════════════════════════════════════════════════
58
+ // climbConsensus / inspectRationale instrumentation.
59
+ //
60
+ // Purely additive tracing over the consensus climb: it never changes an
61
+ // inference result or the human-readable rationale text traceAttention
62
+ // already produces (see below) — it only exposes, as one structured `data`
63
+ // payload on the SAME "climbConsensus" step, the machinery that produced
64
+ // that text: every structural saturation stop, candidate breadth versus
65
+ // evidence that actually contributed, and the decisions that removed or
66
+ // accepted evidence (§ objective of the instrumentation spec).
67
+ //
68
+ // The mutable collection buffers (the `TraceDraft` below) are allocated ONLY
69
+ // when `ctx.trace` is set — every call site that would otherwise push onto
70
+ // one of these arrays or sets is gated by `td?.` / `if (td)`, so a plain
71
+ // (untraced) climb pays exactly zero allocation for this instrumentation.
72
+ // ═══════════════════════════════════════════════════════════════════════════
73
+
74
+ /** How the newly-added graded junction ladder (junction.ts / attention.ts's
75
+ * {@link CrossRegionTier}) is reported on a `junctionVotes` entry. The
76
+ * instrumentation spec this implements predates that ladder and only knew
77
+ * two tiers ("exact" | "synonym"); with the richer `CrossRegionTier` now
78
+ * the real shape of a junction vote's provenance, `junctionVotes[].tier`
79
+ * reports it DIRECTLY (the tier as-is: "exact" | "single-synonym" |
80
+ * "double-synonym" | "structural-resonance") rather than collapsing every
81
+ * non-exact tier into a lossy "synonym" bucket — the whole point of
82
+ * exposing `tier` here is to let a debugger tell a halo-sibling
83
+ * substitution apart from a structural-resonance ANN guess, which the
84
+ * spec's original two-value type cannot do. */
85
+ export type ClimbConsensusJunctionTier = CrossRegionTier;
86
+
87
+ export type RegionOutcome =
88
+ | "voted"
89
+ | "no-ann-hit"
90
+ | "no-structural-reach"
91
+ | "saturated-abstention"
92
+ | "nonpositive-df-weight"
93
+ | "contrastive-margin-rejection";
94
+
95
+ export interface ConsensusRegionTrace {
96
+ index: number;
97
+ source: "perceived" | "recognised";
98
+ span: [number, number];
99
+ chunk: boolean;
100
+ known: boolean;
101
+
102
+ canonicalId?: number;
103
+ canonicalUsable: boolean;
104
+ canonicalFailed: boolean;
105
+
106
+ annQueried: boolean;
107
+ annHitsReturned: number;
108
+ annHitsExamined: number;
109
+
110
+ selected?: {
111
+ source: "canonical" | "ann";
112
+ node: number;
113
+ rank?: number;
114
+ score: number;
115
+ fallback?: "orphan" | "saturated-tie";
116
+ };
117
+
118
+ reachNode?: number;
119
+ outcome: RegionOutcome;
120
+
121
+ idf?: number;
122
+ dfWeight?: number;
123
+
124
+ contrastiveMargin?: number;
125
+ contrastiveNoiseFloor?: number;
126
+
127
+ mutualWeight?: number;
128
+ voteWeightPerRoot?: number;
129
+ focusWeightPerRoot?: number;
130
+
131
+ ordinaryVoteProduced: boolean;
132
+ superseded: boolean;
133
+ }
134
+
135
+ export interface ConsensusReachTrace {
136
+ node: number;
137
+ roots: number[];
138
+ contextsReached: number;
139
+ saturated: boolean;
140
+ saturation?: SaturationStop;
141
+ }
142
+
143
+ export type AnchorRejectionReason =
144
+ | "below-natural-break"
145
+ | "below-consensus-floor"
146
+ | "leading-saturation";
147
+
148
+ export interface ConsensusAnchorTrace {
149
+ anchor: number;
150
+ rank: number;
151
+
152
+ pooledVote: number;
153
+ idfVote: number;
154
+
155
+ candidateBreadth: number;
156
+ contributingVotes: number;
157
+ contributingEvidence: number;
158
+ breadth: number;
159
+ contributingSpans: Array<[number, number]>;
160
+ clusters: number;
161
+
162
+ commit: {
163
+ status: "root" | "overlap" | "rejected";
164
+ dominant: boolean;
165
+ passesNaturalBreak?: boolean;
166
+ passesConsensusFloor?: boolean;
167
+ pastLeadingSaturation?: boolean;
168
+ rejectionReasons: AnchorRejectionReason[];
169
+ };
170
+ }
171
+
172
+ export interface JunctionVoteTrace {
173
+ container: number;
174
+ span: [number, number];
175
+ roots: number[];
176
+ sourceRegionIndices: number[];
177
+ explainedAwayRegionIndices: number[];
178
+ absorbed: number;
179
+ tier?: ClimbConsensusJunctionTier;
180
+ }
181
+
182
+ export interface ClimbConsensusData {
183
+ version: 1;
184
+
185
+ cache: {
186
+ hit: boolean;
187
+ detailAvailable: boolean;
188
+ };
189
+
190
+ config: {
191
+ annK: number;
192
+ crossRegionProbeLimit: number;
193
+ mode: DFMode;
194
+ corpusN?: number;
195
+ dimension?: number;
196
+ hubBound?: number;
197
+ estimatorNoise?: number;
198
+ naturalBreak?: number;
199
+ consensusFloor?: number;
200
+ };
201
+
202
+ candidates: {
203
+ perceived: number;
204
+ recognised: number;
205
+ total: number;
206
+ };
207
+
208
+ regions?: ConsensusRegionTrace[];
209
+ reaches?: ConsensusReachTrace[];
210
+
211
+ crossRegion?: {
212
+ eligibleRegions: number;
213
+ maximalRegions: number;
214
+ probeLimit: number;
215
+ probesAttempted: number;
216
+ junctionVotes: JunctionVoteTrace[];
217
+ supersededOrdinaryVotes: number;
218
+ };
219
+
220
+ saturation?: {
221
+ regionIntervals: Array<{ start: number; end: number }>;
222
+ hasLeading: boolean;
223
+ leadingEnd: number;
224
+ };
225
+
226
+ pooling?: {
227
+ inputVotes: number;
228
+ eligibleVotes: number;
229
+ saturationMaskedVotes: number;
230
+ };
231
+
232
+ anchors?: ConsensusAnchorTrace[];
233
+
234
+ result: AttentionRead;
235
+ }
236
+
237
+ /** The mutable collection buffers threaded through one traced consensus
238
+ * climb — allocated exactly once, in {@link computeAttention}, only when
239
+ * `ctx.trace` is set. Every field mirrors a `ClimbConsensusData` array/map,
240
+ * built incrementally as the pipeline runs so commit-time decisions (in
241
+ * particular) are recorded LIVE, not reconstructed afterward. */
242
+ interface TraceDraft {
243
+ perceivedCount: number;
244
+ regions: ConsensusRegionTrace[];
245
+ crossRegionJunctionVotes: JunctionVoteTrace[];
246
+ crossRegionSummary?: {
247
+ eligibleRegions: number;
248
+ maximalRegions: number;
249
+ probeLimit: number;
250
+ probesAttempted: number;
251
+ };
252
+ supersededOrdinaryVotes: number;
253
+ saturation?: {
254
+ regionIntervals: Array<{ start: number; end: number }>;
255
+ hasLeading: boolean;
256
+ leadingEnd: number;
257
+ };
258
+ pooling?: {
259
+ inputVotes: number;
260
+ eligibleVotes: number;
261
+ saturationMaskedVotes: number;
262
+ };
263
+ anchors: ConsensusAnchorTrace[];
264
+ }
265
+
266
+ /** The config/corpus context {@link traceAttention} needs to fill in
267
+ * `ClimbConsensusData.config` and `.result` at whichever exit fires —
268
+ * threaded down from {@link computeAttention} rather than re-derived, so
269
+ * every emission point reports the SAME numbers the real climb used. */
270
+ interface ClimbConsensusCfg {
271
+ k: number;
272
+ mode: DFMode;
273
+ perceivedCount: number;
274
+ totalRegions: number;
275
+ N?: number;
276
+ reachMemo?: ReadonlyMap<number, AncestorReach>;
277
+ naturalBreak?: number;
278
+ consensusFloor?: number;
279
+ }
280
+
281
+ function newTraceDraft(perceivedCount: number): TraceDraft {
282
+ return {
283
+ perceivedCount,
284
+ regions: [],
285
+ crossRegionJunctionVotes: [],
286
+ supersededOrdinaryVotes: 0,
287
+ anchors: [],
288
+ };
289
+ }
290
+
291
+ /** Serialise the shared `reachMemo` into the plain, authoritative saturation
292
+ * profile (spec §5) — every distinct node any tier's `edgeAncestors` call
293
+ * climbed from during this response, in insertion (first-consulted) order. */
294
+ function serialiseReaches(
295
+ reachMemo: ReadonlyMap<number, AncestorReach>,
296
+ ): ConsensusReachTrace[] {
297
+ const out: ConsensusReachTrace[] = [];
298
+ for (const [node, r] of reachMemo) {
299
+ out.push({
300
+ node,
301
+ roots: [...r.roots],
302
+ contextsReached: r.contextsReached,
303
+ saturated: r.saturated,
304
+ ...(r.saturation ? { saturation: r.saturation } : {}),
305
+ });
306
+ }
307
+ return out;
308
+ }
309
+
45
310
  // ── Public entry points ───────────────────────────────────────────────────
46
311
 
47
312
  /** Climb the query's perceived byte regions up the structural DAG via
@@ -85,12 +350,27 @@ export async function climbAttentionAll(
85
350
  }
86
351
  const hit = byRead.get(modeKey);
87
352
  if (hit !== undefined) {
353
+ // Cache-hit exit (spec §9): the abbreviated payload shape — only what
354
+ // is actually stored in the cached AttentionRead is reported. No
355
+ // candidate, reach, saturation, pooling or anchor detail is fabricated
356
+ // (that per-region detail was never retained by the memo).
357
+ const data: ClimbConsensusData | undefined = ctx.trace
358
+ ? {
359
+ version: 1,
360
+ cache: { hit: true, detailAvailable: false },
361
+ config: { annK: k, crossRegionProbeLimit: k, mode },
362
+ candidates: { perceived: 0, recognised: 0, total: 0 },
363
+ result: hit,
364
+ }
365
+ : undefined;
88
366
  ctx.trace?.step(
89
367
  "climbConsensus",
90
368
  [rItem(query, "query")],
91
369
  hit.roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)),
92
370
  `(cached) consensus already computed for this query — ` +
93
371
  `${hit.roots.length} point(s) of attention`,
372
+ undefined,
373
+ data,
94
374
  );
95
375
  return hit;
96
376
  }
@@ -110,6 +390,7 @@ export async function computeAttention(
110
390
  mode: DFMode,
111
391
  ): Promise<AttentionRead> {
112
392
  const regions = collectRegions(ctx, query);
393
+ const perceivedCount = regions.length;
113
394
 
114
395
  // Recognised sites carry structural evidence that perceived sub-regions
115
396
  // miss: a word crossing a W-boundary is split into chunks whose partial
@@ -130,14 +411,30 @@ export async function computeAttention(
130
411
  });
131
412
  }
132
413
 
133
- if (regions.length === 0) return { roots: [], ranked: [] };
414
+ // The trace draft (spec §9): allocated ONLY when a trace was requested —
415
+ // every downstream consumer gates its own writes on `td?` / `if (td)`, so
416
+ // an untraced climb pays zero allocation for this instrumentation.
417
+ const td: TraceDraft | undefined = ctx.trace
418
+ ? newTraceDraft(perceivedCount)
419
+ : undefined;
420
+ const cfg0: ClimbConsensusCfg = {
421
+ k,
422
+ mode,
423
+ perceivedCount,
424
+ totalRegions: regions.length,
425
+ };
426
+
427
+ if (regions.length === 0) {
428
+ traceAttention(ctx, [], [], [], undefined, td, cfg0);
429
+ return { roots: [], ranked: [] };
430
+ }
134
431
 
135
432
  const N = corpusN(ctx);
136
433
  // One climb per distinct anchor for the WHOLE query: regions sharing a
137
434
  // chunk, and canonicalChunkId's prefix probes, all hit this memo instead of
138
435
  // re-reading the anchor's full edge fan-out from the store.
139
436
  const reachMemo = new Map<number, AncestorReach>();
140
- const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo);
437
+ const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo, td);
141
438
 
142
439
  // ── Cross-region: DIRECT region-to-region interaction ─────────────────
143
440
  // Two regions whose individual climbs land on DIFFERENT contexts leave
@@ -152,6 +449,7 @@ export async function computeAttention(
152
449
  k,
153
450
  N,
154
451
  reachMemo,
452
+ td,
155
453
  );
156
454
  // A vote SUPERSEDED by exact joint evidence (its bytes literally live
157
455
  // inside the joint container, yet it climbed elsewhere — grid aliasing)
@@ -162,16 +460,37 @@ export async function computeAttention(
162
460
  ...cross.votes,
163
461
  ]
164
462
  : rvs.votes;
463
+ // Mark, on the per-region trace, the source region of every superseded
464
+ // ordinary vote (spec §4's final rule) — an explicit pass over the exact
465
+ // set crossRegionVotes' explaining-away logic removed, never inferred
466
+ // from `absorbed`.
467
+ if (td && cross.superseded.size > 0) {
468
+ for (const rv of cross.superseded) {
469
+ const region = td.regions.find(
470
+ (r) => r.span[0] === rv.start && r.span[1] === rv.end,
471
+ );
472
+ if (region) region.superseded = true;
473
+ }
474
+ }
165
475
  // ──────────────────────────────────────────────────────────────────────
166
476
 
477
+ const cfg: ClimbConsensusCfg = { ...cfg0, N, reachMemo };
478
+
167
479
  if (allVotes.length === 0) {
168
- traceAttention(ctx, regions, rvs.voters, []);
480
+ traceAttention(ctx, regions, rvs.voters, [], undefined, td, cfg);
169
481
  return { roots: [], ranked: [] };
170
482
  }
171
483
 
172
484
  const sat = detectSaturated(ctx, regions, rvs.saturated);
173
- const pooled = poolVotes(ctx, allVotes, sat, N);
174
- return commitVotes(ctx, pooled, sat, regions, rvs.voters, N);
485
+ if (td) {
486
+ td.saturation = {
487
+ regionIntervals: sat.intervals.map((iv) => ({ ...iv })),
488
+ hasLeading: sat.hasLeading,
489
+ leadingEnd: sat.leadingEnd,
490
+ };
491
+ }
492
+ const pooled = poolVotes(ctx, allVotes, sat, N, td);
493
+ return commitVotes(ctx, pooled, sat, regions, rvs.voters, N, td, cfg);
175
494
  }
176
495
 
177
496
  export function collectRegions(ctx: MindContext, query: Uint8Array): Region[] {
@@ -212,6 +531,7 @@ export async function voteRegions(
212
531
  mode: DFMode,
213
532
  N: number,
214
533
  reachMemo?: Map<number, AncestorReach>,
534
+ td?: TraceDraft,
215
535
  ): Promise<{
216
536
  votes: RegionVote[];
217
537
  saturated: boolean[];
@@ -224,6 +544,37 @@ export async function voteRegions(
224
544
 
225
545
  for (let ri = 0; ri < regions.length; ri++) {
226
546
  const { v, start, end, chunk, known } = regions[ri];
547
+ // Trace-only bookkeeping for this region — allocated only under `td`
548
+ // (i.e. only when ctx.trace is set); see ConsensusRegionTrace/
549
+ // RegionOutcome (spec §4). `examinedIds` tracks distinct ANN hits
550
+ // whose edgeAncestors reach was actually CONSULTED here (not merely
551
+ // returned by resonate) — the fallback/margin loops below add to it.
552
+ const examinedIds = td ? new Set<number>() : undefined;
553
+ let annQueried = false;
554
+ let fallbackKind: "orphan" | "saturated-tie" | undefined;
555
+ const recordRegion = (
556
+ outcome: RegionOutcome,
557
+ extra: Partial<ConsensusRegionTrace> = {},
558
+ ) => {
559
+ if (!td) return;
560
+ td.regions[ri] = {
561
+ index: ri,
562
+ source: ri < td.perceivedCount ? "perceived" : "recognised",
563
+ span: [start, end],
564
+ chunk,
565
+ known,
566
+ canonicalId: canonicalId ?? undefined,
567
+ canonicalUsable,
568
+ canonicalFailed,
569
+ annQueried,
570
+ annHitsReturned: hits ? hits.length : 0,
571
+ annHitsExamined: examinedIds ? examinedIds.size : 0,
572
+ outcome,
573
+ ordinaryVoteProduced: outcome === "voted",
574
+ superseded: false,
575
+ ...extra,
576
+ };
577
+ };
227
578
 
228
579
  // EXACT-FIRST: a chunk whose canonical anchor is content-addressed needs
229
580
  // no estimator — identity is exact, so its score is 1 BY DEFINITION (the
@@ -243,23 +594,35 @@ export async function voteRegions(
243
594
  (ctx.store.hasParents(canonicalId) ||
244
595
  ctx.store.hasContainers(canonicalId));
245
596
  let hits: readonly Hit[] | null = null;
246
- const ensureHits = async (): Promise<readonly Hit[]> =>
247
- hits ??= await ctx.store.resonate(v, k);
597
+ const ensureHits = async (): Promise<readonly Hit[]> => {
598
+ if (hits === null) {
599
+ hits = await ctx.store.resonate(v, k);
600
+ annQueried = true;
601
+ }
602
+ return hits;
603
+ };
248
604
 
249
605
  const canonicalFailed = chunk && canonicalId === null;
250
606
  let voterId: number;
251
607
  let score: number;
252
608
  let scoreId: number; // the node the score was measured against
609
+ let selectedSource: "canonical" | "ann";
253
610
  if (canonicalUsable) {
254
611
  voterId = canonicalId!;
255
612
  score = 1;
256
613
  scoreId = canonicalId!;
614
+ selectedSource = "canonical";
257
615
  } else {
258
616
  const h = await ensureHits();
259
- if (h.length === 0) continue;
617
+ if (h.length === 0) {
618
+ recordRegion("no-ann-hit");
619
+ continue;
620
+ }
260
621
  voterId = h[0].id;
261
622
  score = h[0].score;
262
623
  scoreId = h[0].id;
624
+ selectedSource = "ann";
625
+ examinedIds?.add(voterId);
263
626
  }
264
627
 
265
628
  let reach = edgeAncestors(ctx, voterId, N, reachMemo);
@@ -273,6 +636,7 @@ export async function voteRegions(
273
636
  for (const h of await ensureHits()) {
274
637
  if (h.id === voterId) continue;
275
638
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
639
+ examinedIds?.add(h.id);
276
640
  if (r2.saturated || r2.roots.length > 0) {
277
641
  ctx.trace?.step(
278
642
  "anchorFallback",
@@ -284,6 +648,8 @@ export async function voteRegions(
284
648
  voterId = h.id;
285
649
  score = h.score;
286
650
  scoreId = h.id;
651
+ selectedSource = "ann";
652
+ fallbackKind = "orphan";
287
653
  break;
288
654
  }
289
655
  }
@@ -307,6 +673,7 @@ export async function voteRegions(
307
673
  if (h.id === voterId) continue;
308
674
  if (h.score < score - band) break; // hits are nearest-first
309
675
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
676
+ examinedIds?.add(h.id);
310
677
  if (!r2.saturated && r2.roots.length > 0) {
311
678
  ctx.trace?.step(
312
679
  "anchorFallback",
@@ -318,20 +685,52 @@ export async function voteRegions(
318
685
  voterId = h.id;
319
686
  score = h.score;
320
687
  scoreId = h.id;
688
+ selectedSource = "ann";
689
+ fallbackKind = "saturated-tie";
321
690
  break;
322
691
  }
323
692
  }
324
693
  }
325
694
  regionSaturated[ri] = reach.saturated;
326
- if (reach.roots.length === 0) continue;
327
- if (reach.saturated) continue;
695
+ const selected: ConsensusRegionTrace["selected"] | undefined = !td
696
+ ? undefined
697
+ : (() => {
698
+ const rank: number | undefined = selectedSource === "ann"
699
+ ? (hits as readonly Hit[] | null)?.findIndex((h: Hit) =>
700
+ h.id === voterId
701
+ )
702
+ : undefined;
703
+ return {
704
+ source: selectedSource,
705
+ node: voterId,
706
+ score,
707
+ ...(rank !== undefined ? { rank } : {}),
708
+ ...(fallbackKind ? { fallback: fallbackKind } : {}),
709
+ };
710
+ })();
711
+ if (reach.roots.length === 0) {
712
+ recordRegion("no-structural-reach", { selected, reachNode: voterId });
713
+ continue;
714
+ }
715
+ if (reach.saturated) {
716
+ recordRegion("saturated-abstention", { selected, reachNode: voterId });
717
+ continue;
718
+ }
328
719
 
329
720
  // One IDF per region — dfWeight() and the focus weight used to compute
330
721
  // the same logarithm independently.
331
722
  const idf = Math.log(N / Math.max(1, reach.contextsReached));
332
723
  const df = Math.log(1 + reach.contextsReached);
333
724
  const wf = mode === "direct" ? df : mode === "combined" ? idf + df : idf;
334
- if (wf <= 0) continue;
725
+ if (wf <= 0) {
726
+ recordRegion("nonpositive-df-weight", {
727
+ selected,
728
+ reachNode: voterId,
729
+ idf,
730
+ dfWeight: wf,
731
+ });
732
+ continue;
733
+ }
335
734
 
336
735
  // CONTRASTIVE-MARGIN GATE — the compensation the linear (byte-proportional)
337
736
  // fold demands, applied to APPROXIMATE evidence only. Under the linear
@@ -356,17 +755,31 @@ export async function voteRegions(
356
755
  // (reordered / paraphrased queries) below the floor so they grounded
357
756
  // nothing. Gating at the noise floor keeps frame-echo suppression (a frame
358
757
  // region's margin ≈ 0 is gated out) without penalising honest evidence.
758
+ let contrastiveMargin: number | undefined;
359
759
  if (!known) {
360
760
  let margin = score;
361
761
  for (const h of await ensureHits()) {
362
762
  if (h.id === voterId) continue;
363
763
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
764
+ examinedIds?.add(h.id);
364
765
  if (r2.saturated || r2.roots.length === 0) continue; // concludes nothing
365
766
  if (sameRoots(r2.roots, reach.roots)) continue; // same conclusion
366
767
  margin = score - h.score; // hits are nearest-first: the best rival
367
768
  break;
368
769
  }
369
- if (margin <= estimatorNoise(ctx.store.D)) continue;
770
+ contrastiveMargin = margin;
771
+ const noiseFloor = estimatorNoise(ctx.store.D);
772
+ if (margin <= noiseFloor) {
773
+ recordRegion("contrastive-margin-rejection", {
774
+ selected,
775
+ reachNode: voterId,
776
+ idf,
777
+ dfWeight: wf,
778
+ contrastiveMargin: margin,
779
+ contrastiveNoiseFloor: noiseFloor,
780
+ });
781
+ continue;
782
+ }
370
783
  }
371
784
 
372
785
  // MUTUAL-EXPLANATION WEIGHT (angle + magnitude). Under the linear fold
@@ -406,6 +819,21 @@ export async function voteRegions(
406
819
  if (ctx.trace) {
407
820
  regionVoter[ri] = { id: voterId, score, w: wf };
408
821
  }
822
+ recordRegion("voted", {
823
+ selected,
824
+ reachNode: voterId,
825
+ idf,
826
+ dfWeight: wf,
827
+ ...(contrastiveMargin !== undefined
828
+ ? {
829
+ contrastiveMargin,
830
+ contrastiveNoiseFloor: estimatorNoise(ctx.store.D),
831
+ }
832
+ : {}),
833
+ mutualWeight: mutual,
834
+ voteWeightPerRoot: w,
835
+ focusWeightPerRoot: wFocus,
836
+ });
409
837
  }
410
838
  return {
411
839
  votes: regionVotes,
@@ -431,6 +859,7 @@ export function poolVotes(
431
859
  regionVotes: readonly RegionVote[],
432
860
  sat: SaturationInfo,
433
861
  N: number,
862
+ td?: TraceDraft,
434
863
  ): {
435
864
  votes: Map<number, number>;
436
865
  votesIdf: Map<number, number>;
@@ -453,6 +882,13 @@ export function poolVotes(
453
882
  }
454
883
  eligible.push(ri);
455
884
  }
885
+ if (td) {
886
+ td.pooling = {
887
+ inputVotes: regionVotes.length,
888
+ eligibleVotes: eligible.length,
889
+ saturationMaskedVotes: regionVotes.length - eligible.length,
890
+ };
891
+ }
456
892
 
457
893
  const key = (it: AItem) =>
458
894
  it.kind === "region"
@@ -612,11 +1048,13 @@ export function commitVotes(
612
1048
  regions: readonly Region[],
613
1049
  regionVoter: ReadonlyArray<{ id: number; score: number; w: number } | null>,
614
1050
  N: number,
1051
+ td?: TraceDraft,
1052
+ cfg?: ClimbConsensusCfg,
615
1053
  ): AttentionRead {
616
1054
  const { votes, votesIdf, support, regionSupport, regionSpans, steps } =
617
1055
  pooled;
618
1056
  if (votes.size === 0) {
619
- traceAttention(ctx, regions, regionVoter, [], steps);
1057
+ traceAttention(ctx, regions, regionVoter, [], steps, td, cfg);
620
1058
  return { roots: [], ranked: [] };
621
1059
  }
622
1060
 
@@ -658,25 +1096,121 @@ export function commitVotes(
658
1096
  const floor = consensusFloor(N);
659
1097
  const placed: Attention[] = [];
660
1098
  const roots: Attention[] = [];
661
- for (const point of ranked) {
1099
+ const recordAnchor = (
1100
+ point: Attention,
1101
+ rank: number,
1102
+ status: "root" | "overlap" | "rejected",
1103
+ dominant: boolean,
1104
+ passesNaturalBreak: boolean | undefined,
1105
+ passesConsensusFloor: boolean | undefined,
1106
+ pastLeadingSaturation: boolean | undefined,
1107
+ rejectionReasons: AnchorRejectionReason[],
1108
+ ) => {
1109
+ if (!td) return;
1110
+ td.anchors.push({
1111
+ anchor: point.anchor,
1112
+ rank,
1113
+ pooledVote: point.vote,
1114
+ idfVote: votesIdf.get(point.anchor) ?? 0,
1115
+ candidateBreadth: regions.length,
1116
+ contributingVotes: regionSpans.get(point.anchor)?.length ?? 0,
1117
+ contributingEvidence: regionSupport.get(point.anchor) ?? 0,
1118
+ breadth: point.breadth,
1119
+ contributingSpans: regionSpans.get(point.anchor) ?? [],
1120
+ clusters: point.clusters,
1121
+ commit: {
1122
+ status,
1123
+ dominant,
1124
+ passesNaturalBreak,
1125
+ passesConsensusFloor,
1126
+ pastLeadingSaturation,
1127
+ rejectionReasons,
1128
+ },
1129
+ });
1130
+ };
1131
+ for (let rank = 0; rank < ranked.length; rank++) {
1132
+ const point = ranked[rank];
662
1133
  const absorbed = placed.some((p) => overlaps(point, p));
663
- if (!absorbed) {
1134
+ // Commit decisions are recorded LIVE, inside this loop, in the exact
1135
+ // shape the gates below apply them — never reconstructed afterward from
1136
+ // the final `roots` (spec §8's explicit requirement).
1137
+ let status: "root" | "overlap" | "rejected";
1138
+ let dominant = false;
1139
+ let passesNaturalBreak: boolean | undefined;
1140
+ let passesConsensusFloor: boolean | undefined;
1141
+ let pastLeadingSaturation: boolean | undefined;
1142
+ const rejectionReasons: AnchorRejectionReason[] = [];
1143
+ if (absorbed) {
1144
+ status = "overlap";
1145
+ } else {
664
1146
  const pastLeading = !sat.hasLeading ||
665
1147
  roots.length === 0 || point.start >= sat.leadingEnd;
1148
+ pastLeadingSaturation = pastLeading;
666
1149
  const vote = votesIdf.get(point.anchor) ?? 0;
667
- if (
668
- (roots.length === 0 || (vote >= rootCut && vote >= floor)) &&
669
- pastLeading
670
- ) {
1150
+ if (roots.length === 0) {
1151
+ // The first non-overlapping root is DOMINANT and bypasses the two
1152
+ // vote thresholds (it always grounds) — only the leading-saturation
1153
+ // gate still applies to it.
1154
+ dominant = true;
1155
+ if (pastLeading) {
1156
+ status = "root";
1157
+ } else {
1158
+ status = "rejected";
1159
+ rejectionReasons.push("leading-saturation");
1160
+ }
1161
+ } else {
1162
+ passesNaturalBreak = vote >= rootCut;
1163
+ passesConsensusFloor = vote >= floor;
1164
+ if (passesNaturalBreak && passesConsensusFloor && pastLeading) {
1165
+ status = "root";
1166
+ } else {
1167
+ status = "rejected";
1168
+ if (!passesNaturalBreak) rejectionReasons.push("below-natural-break");
1169
+ if (!passesConsensusFloor) {
1170
+ rejectionReasons.push("below-consensus-floor");
1171
+ }
1172
+ if (!pastLeading) rejectionReasons.push("leading-saturation");
1173
+ }
1174
+ }
1175
+ if (status === "root") {
671
1176
  roots.push(point);
672
1177
  } else {
1178
+ recordAnchor(
1179
+ point,
1180
+ rank,
1181
+ status,
1182
+ dominant,
1183
+ passesNaturalBreak,
1184
+ passesConsensusFloor,
1185
+ pastLeadingSaturation,
1186
+ rejectionReasons,
1187
+ );
673
1188
  continue;
674
1189
  }
675
1190
  }
1191
+ recordAnchor(
1192
+ point,
1193
+ rank,
1194
+ status,
1195
+ dominant,
1196
+ passesNaturalBreak,
1197
+ passesConsensusFloor,
1198
+ pastLeadingSaturation,
1199
+ rejectionReasons,
1200
+ );
676
1201
  placed.push(point);
677
1202
  }
678
1203
 
679
- traceAttention(ctx, regions, regionVoter, roots, steps);
1204
+ traceAttention(
1205
+ ctx,
1206
+ regions,
1207
+ regionVoter,
1208
+ roots,
1209
+ steps,
1210
+ td,
1211
+ cfg ? { ...cfg, naturalBreak: rootCut, consensusFloor: floor } : undefined,
1212
+ ranked,
1213
+ );
680
1214
  return { roots, ranked };
681
1215
  }
682
1216
 
@@ -843,6 +1377,266 @@ export function naturalBreak(votes: number[]): number {
843
1377
  // container does not hold (a genuine second topic) are untouched.
844
1378
  // ═══════════════════════════════════════════════════════════════════════════
845
1379
 
1380
+ // ── Structural-resonance — the FINAL approximate tier ──────────────────────
1381
+ //
1382
+ // Reached only when every DAG junction tier (exact, single-synonym, double-
1383
+ // synonym) found no container. Composes a hypothetical structural gist from
1384
+ // ALREADY-EXISTING structural vectors — the two endpoint regions' own gists
1385
+ // (or, per variant, a halo sibling's stored gist occupying the same slot)
1386
+ // plus the REAL middle-query structure between them — and asks the ANN index
1387
+ // what already-learnt whole resembles that composition. It never perceives
1388
+ // concatenated endpoint bytes and never fabricates a rewritten query string;
1389
+ // see {@link composeStructuralGist}.
1390
+
1391
+ export type CrossRegionTier =
1392
+ | "exact"
1393
+ | "single-synonym"
1394
+ | "double-synonym"
1395
+ | "structural-resonance";
1396
+
1397
+ export interface StructuralVariant {
1398
+ left: StructuralPart;
1399
+ right: StructuralPart;
1400
+ kind:
1401
+ | "exact-exact"
1402
+ | "left-synonym"
1403
+ | "right-synonym"
1404
+ | "double-synonym";
1405
+ semanticConfidence: number;
1406
+ leftSiblingId?: number;
1407
+ rightSiblingId?: number;
1408
+ }
1409
+
1410
+ export interface StructuralResonanceProposal {
1411
+ id: number;
1412
+ annScore: number;
1413
+ semanticConfidence: number;
1414
+ effectiveScore: number;
1415
+ variant: StructuralVariant["kind"];
1416
+ leftSiblingId?: number;
1417
+ rightSiblingId?: number;
1418
+ }
1419
+
1420
+ const VARIANT_KIND_ORDER: Record<StructuralVariant["kind"], number> = {
1421
+ "exact-exact": -1,
1422
+ "left-synonym": 0,
1423
+ "right-synonym": 1,
1424
+ "double-synonym": 2,
1425
+ };
1426
+
1427
+ /** A node's structural gist, read directly from its own stored bytes — the
1428
+ * repository's node → gist accessor: content is immutable and perception is
1429
+ * a pure function of bytes, so re-perceiving a node's full stored bytes
1430
+ * reproduces exactly the gist it was interned with. Never concatenates
1431
+ * the sibling's bytes with anything else. */
1432
+ function storedNodeGist(ctx: MindContext, id: number): Vec {
1433
+ return gistOf(ctx, read(ctx, id));
1434
+ }
1435
+
1436
+ /** A halo sibling's structural part, occupying the ORIGINAL query-region
1437
+ * slot length — the sibling replaces only the DIRECTION, never the query's
1438
+ * own bytes, position, or gap (see §6 of the spec this implements). */
1439
+ function siblingPart(
1440
+ ctx: MindContext,
1441
+ sibling: Hit,
1442
+ originalRegion: { start: number; end: number },
1443
+ ): StructuralPart {
1444
+ return {
1445
+ v: storedNodeGist(ctx, sibling.id),
1446
+ len: originalRegion.end - originalRegion.start,
1447
+ };
1448
+ }
1449
+
1450
+ /** Build, bound and order every mandatory structural variant (§7-8): the
1451
+ * exact/exact composition is always kept; up to `ctx.cfg.haloQueryK`
1452
+ * synonym variants (single- and double-synonym combined, one shared
1453
+ * budget) are appended, ordered by confidence, then kind, then sibling id. */
1454
+ export function buildStructuralVariants(
1455
+ ctx: MindContext,
1456
+ ra: Region,
1457
+ rb: Region,
1458
+ sides: JunctionSynonymSides,
1459
+ ): {
1460
+ variants: StructuralVariant[];
1461
+ exactLeft: StructuralPart;
1462
+ exactRight: StructuralPart;
1463
+ } {
1464
+ const exactLeft: StructuralPart = { v: ra.v, len: ra.end - ra.start };
1465
+ const exactRight: StructuralPart = { v: rb.v, len: rb.end - rb.start };
1466
+
1467
+ const synonymVariants: StructuralVariant[] = [];
1468
+ for (const ls of sides.leftSiblings) {
1469
+ synonymVariants.push({
1470
+ left: siblingPart(ctx, ls, ra),
1471
+ right: exactRight,
1472
+ kind: "left-synonym",
1473
+ semanticConfidence: ls.score,
1474
+ leftSiblingId: ls.id,
1475
+ });
1476
+ }
1477
+ for (const rs of sides.rightSiblings) {
1478
+ synonymVariants.push({
1479
+ left: exactLeft,
1480
+ right: siblingPart(ctx, rs, rb),
1481
+ kind: "right-synonym",
1482
+ semanticConfidence: rs.score,
1483
+ rightSiblingId: rs.id,
1484
+ });
1485
+ }
1486
+ for (const ls of sides.leftSiblings) {
1487
+ for (const rs of sides.rightSiblings) {
1488
+ synonymVariants.push({
1489
+ left: siblingPart(ctx, ls, ra),
1490
+ right: siblingPart(ctx, rs, rb),
1491
+ kind: "double-synonym",
1492
+ semanticConfidence: Math.min(ls.score, rs.score),
1493
+ leftSiblingId: ls.id,
1494
+ rightSiblingId: rs.id,
1495
+ });
1496
+ }
1497
+ }
1498
+
1499
+ // §8: semantic confidence desc, then kind (left-synonym, right-synonym,
1500
+ // double-synonym), then left sibling id asc, then right sibling id asc.
1501
+ synonymVariants.sort((a, b) =>
1502
+ b.semanticConfidence - a.semanticConfidence ||
1503
+ VARIANT_KIND_ORDER[a.kind] - VARIANT_KIND_ORDER[b.kind] ||
1504
+ (a.leftSiblingId ?? -1) - (b.leftSiblingId ?? -1) ||
1505
+ (a.rightSiblingId ?? -1) - (b.rightSiblingId ?? -1)
1506
+ );
1507
+
1508
+ const variants: StructuralVariant[] = [
1509
+ {
1510
+ left: exactLeft,
1511
+ right: exactRight,
1512
+ kind: "exact-exact",
1513
+ semanticConfidence: 1,
1514
+ },
1515
+ ...synonymVariants.slice(0, ctx.cfg.haloQueryK),
1516
+ ];
1517
+ return { variants, exactLeft, exactRight };
1518
+ }
1519
+
1520
+ /** Deterministic best-of tie-break for two proposals ranked for the SAME
1521
+ * candidate id — effectiveScore, then annScore, then semanticConfidence,
1522
+ * then variant kind, then sibling ids (§10). */
1523
+ function betterProposal(
1524
+ a: StructuralResonanceProposal,
1525
+ b: StructuralResonanceProposal,
1526
+ ): boolean {
1527
+ if (a.effectiveScore !== b.effectiveScore) {
1528
+ return a.effectiveScore > b.effectiveScore;
1529
+ }
1530
+ if (a.annScore !== b.annScore) return a.annScore > b.annScore;
1531
+ if (a.semanticConfidence !== b.semanticConfidence) {
1532
+ return a.semanticConfidence > b.semanticConfidence;
1533
+ }
1534
+ if (VARIANT_KIND_ORDER[a.variant] !== VARIANT_KIND_ORDER[b.variant]) {
1535
+ return VARIANT_KIND_ORDER[a.variant] < VARIANT_KIND_ORDER[b.variant];
1536
+ }
1537
+ if ((a.leftSiblingId ?? -1) !== (b.leftSiblingId ?? -1)) {
1538
+ return (a.leftSiblingId ?? -1) < (b.leftSiblingId ?? -1);
1539
+ }
1540
+ return (a.rightSiblingId ?? -1) < (b.rightSiblingId ?? -1);
1541
+ }
1542
+
1543
+ /** The final approximate tier: compose every retained structural variant,
1544
+ * ANN-query each, merge proposals by candidate id, and validate the winner
1545
+ * through the SAME structural gates every other tier answers to (saturation,
1546
+ * roots, IDF, contrastive margin). Returns null when nothing survives. */
1547
+ export async function structuralResonance(
1548
+ ctx: MindContext,
1549
+ query: Uint8Array,
1550
+ ra: Region,
1551
+ rb: Region,
1552
+ sides: JunctionSynonymSides,
1553
+ k: number,
1554
+ N: number,
1555
+ reachMemo: Map<number, AncestorReach>,
1556
+ /** Each side's OWN individual climb roots (from voteRegions), when it cast
1557
+ * one — the self-evidence backstop structural-resonance needs and the
1558
+ * exact tier gets for free from literal byte containment (§11's whole
1559
+ * premise: recover a JOINT context neither side votes for alone). A
1560
+ * candidate whose reach is exactly one side's own conclusion is not new
1561
+ * evidence of a joint whole; it is that side's resonance rediscovering
1562
+ * itself through a synthetic gist still dominated by its own direction. */
1563
+ ownRootsA: readonly number[] | undefined,
1564
+ ownRootsB: readonly number[] | undefined,
1565
+ ): Promise<
1566
+ | { proposal: StructuralResonanceProposal; reach: AncestorReach; idf: number }
1567
+ | null
1568
+ > {
1569
+ const { variants } = buildStructuralVariants(ctx, ra, rb, sides);
1570
+
1571
+ const middleBytes = query.subarray(ra.end, rb.start);
1572
+ const middlePart: StructuralPart | null = middleBytes.length === 0
1573
+ ? null
1574
+ : { v: perceive(ctx, middleBytes).v, len: middleBytes.length };
1575
+
1576
+ const proposals = new Map<number, StructuralResonanceProposal>();
1577
+ for (const variant of variants) {
1578
+ const parts: StructuralPart[] = [variant.left];
1579
+ if (middlePart) parts.push(middlePart);
1580
+ parts.push(variant.right);
1581
+ const synthetic = composeStructuralGist(ctx.space, parts);
1582
+ const hits = await ctx.store.resonate(synthetic, k);
1583
+ for (const hit of hits) {
1584
+ const candidate: StructuralResonanceProposal = {
1585
+ id: hit.id,
1586
+ annScore: hit.score,
1587
+ semanticConfidence: variant.semanticConfidence,
1588
+ effectiveScore: hit.score * variant.semanticConfidence,
1589
+ variant: variant.kind,
1590
+ leftSiblingId: variant.leftSiblingId,
1591
+ rightSiblingId: variant.rightSiblingId,
1592
+ };
1593
+ const prev = proposals.get(hit.id);
1594
+ if (prev === undefined || betterProposal(candidate, prev)) {
1595
+ proposals.set(hit.id, candidate);
1596
+ }
1597
+ }
1598
+ }
1599
+ if (proposals.size === 0) return null;
1600
+
1601
+ const sorted = [...proposals.values()].sort((a, b) =>
1602
+ b.effectiveScore - a.effectiveScore || a.id - b.id
1603
+ );
1604
+
1605
+ let selected: StructuralResonanceProposal | null = null;
1606
+ let selectedReach: AncestorReach | null = null;
1607
+ let selectedIdf = 0;
1608
+ let rival: StructuralResonanceProposal | null = null;
1609
+ for (const p of sorted) {
1610
+ const reach = edgeAncestors(ctx, p.id, N, reachMemo);
1611
+ if (reach.saturated || reach.roots.length === 0) continue;
1612
+ const idf = Math.log(N / Math.max(1, reach.contextsReached));
1613
+ if (idf <= 0) continue;
1614
+ // Self-evidence backstop (see the param doc above): a candidate that is
1615
+ // exactly one side's own already-voted conclusion carries no JOINT
1616
+ // evidence — skip it as if it never survived.
1617
+ if (
1618
+ (ownRootsA && sameRoots(reach.roots, ownRootsA)) ||
1619
+ (ownRootsB && sameRoots(reach.roots, ownRootsB))
1620
+ ) continue;
1621
+ if (selected === null) {
1622
+ selected = p;
1623
+ selectedReach = reach;
1624
+ selectedIdf = idf;
1625
+ } else if (!sameRoots(reach.roots, selectedReach!.roots)) {
1626
+ rival = p;
1627
+ break;
1628
+ }
1629
+ }
1630
+ if (selected === null || selectedReach === null) return null;
1631
+
1632
+ const margin = rival
1633
+ ? selected.effectiveScore - rival.effectiveScore
1634
+ : selected.effectiveScore;
1635
+ if (margin <= estimatorNoise(ctx.store.D)) return null;
1636
+
1637
+ return { proposal: selected, reach: selectedReach, idf: selectedIdf };
1638
+ }
1639
+
846
1640
  async function crossRegionVotes(
847
1641
  ctx: MindContext,
848
1642
  query: Uint8Array,
@@ -851,6 +1645,7 @@ async function crossRegionVotes(
851
1645
  k: number,
852
1646
  N: number,
853
1647
  reachMemo: Map<number, AncestorReach>,
1648
+ td?: TraceDraft,
854
1649
  ): Promise<{ votes: RegionVote[]; superseded: Set<RegionVote> }> {
855
1650
  // Candidate regions: every region that ALREADY CAST ITS OWN VOTE in
856
1651
  // voteRegions — individually idf > 0, genuinely discriminative on its own,
@@ -902,6 +1697,14 @@ async function crossRegionVotes(
902
1697
  )
903
1698
  );
904
1699
  const none = { votes: [], superseded: new Set<RegionVote>() };
1700
+ if (td) {
1701
+ td.crossRegionSummary = {
1702
+ eligibleRegions: eligible.length,
1703
+ maximalRegions: cand.length,
1704
+ probeLimit: k,
1705
+ probesAttempted: 0, // updated below as probes accrue
1706
+ };
1707
+ }
905
1708
  if (cand.length < 2) return none;
906
1709
  cand.sort((x, y) =>
907
1710
  regions[x].start - regions[y].start || regions[x].end - regions[y].end
@@ -972,6 +1775,9 @@ async function crossRegionVotes(
972
1775
  regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end)
973
1776
  ) continue;
974
1777
  probes++;
1778
+ if (td?.crossRegionSummary) {
1779
+ td.crossRegionSummary.probesAttempted = probes;
1780
+ }
975
1781
 
976
1782
  const left = query.subarray(ra.start, ra.end);
977
1783
  const right = query.subarray(rb.start, rb.end);
@@ -980,112 +1786,218 @@ async function crossRegionVotes(
980
1786
  const maxInterior = (left.length + right.length) * ctx.space.maxGroup;
981
1787
  const cap = left.length + right.length + maxInterior;
982
1788
 
983
- // Tier 1 exact containers (both forms as substrings, either order, by
984
- // DAG ascent). Exact evidence first; only falls through to synonyms
985
- // when the exact ascent finds nothing the SAME graded ladder the
986
- // bridge uses.
987
- let containers = junctionContainersFrom(
988
- ctx,
989
- left,
990
- right,
991
- cap,
992
- seedsOf(cand[a]),
993
- seedsOf(cand[b]),
994
- undefined,
995
- true,
996
- );
1789
+ // The graded ladder (spec §1): exact DAG junction, then single-synonym,
1790
+ // then double-synonym, then only when every DAG tier found nothing —
1791
+ // structural-resonance. `sides` (the two halo sibling lists) is loaded
1792
+ // ONCE and reused by junctionSynonyms AND structural-resonance, so no
1793
+ // ladder rung repeats a halo ANN query an earlier rung already paid for.
1794
+ const sides = await loadJunctionSynonymSides(ctx, left, right);
1795
+
1796
+ let tier: CrossRegionTier = "exact";
1797
+ let containers: Array<Junction | SynonymJunction> =
1798
+ junctionContainersFrom(
1799
+ ctx,
1800
+ left,
1801
+ right,
1802
+ cap,
1803
+ seedsOf(cand[a]),
1804
+ seedsOf(cand[b]),
1805
+ undefined,
1806
+ true,
1807
+ );
997
1808
  if (containers.length === 0) {
998
- // Tier 2.5 — synonym containers (halo sibling of one side + the other).
999
- containers = await junctionSynonyms(
1809
+ // Tiers 2-4 — synonym containers (junctionSynonyms itself runs
1810
+ // single-synonym first, falling to double-synonym only when
1811
+ // single-synonym found nothing — see junction.ts).
1812
+ const syn = await junctionSynonyms(
1000
1813
  ctx,
1001
1814
  left,
1002
1815
  right,
1003
1816
  maxInterior,
1004
1817
  true,
1818
+ sides,
1005
1819
  );
1820
+ if (syn.length > 0) {
1821
+ containers = syn;
1822
+ tier = syn[0].tier;
1823
+ }
1006
1824
  }
1007
- if (containers.length === 0) continue;
1008
-
1009
- // N-ARY selection: the container covering the MOST remaining candidate
1010
- // forms wins (then tightest interior, then lowest id). Reads are
1011
- // cache hits every container's bytes were already read by the walk.
1012
- //
1013
- // SELF-EVIDENCE GUARD: a junction is BINDING evidence only when the
1014
- // container joins forms the query mentions APART. When the container's
1015
- // own joined occurrence (left..right including its interior) is a
1016
- // literal substring of the query, the query already spells that phrase
1017
- // out contiguously — perception already voted with it, and grid shards
1018
- // of one phrase pairing "around" a gap chunk would merely rediscover
1019
- // the phrase they are shards of, then explain away its rivals.
1020
- let best: (typeof containers)[number] | null = null;
1021
- let bestExtras: number[] = [];
1022
- let bestCov = -1;
1023
- for (const c of containers) {
1024
- const bytes = cachedRead(ctx, cache, c.id, cap);
1025
- const li = indexOf(bytes, left, 0);
1026
- const ri = indexOf(bytes, right, 0);
1027
- if (li >= 0 && ri >= 0) {
1028
- const joined = bytes.subarray(
1029
- Math.min(li, ri),
1030
- Math.max(li + left.length, ri + right.length),
1031
- );
1032
- if (indexOf(query, joined, 0) >= 0) continue; // query says it itself
1825
+
1826
+ // Tier 5 — structural-resonance ANN, the FINAL approximate proposal
1827
+ // path. Only reached when every DAG tier found NOTHING, and only when
1828
+ // there is no already-corroborated region between the endpoints (a
1829
+ // between-region with its own vote is evidence the gap already means
1830
+ // something specific — an ANN guess must not override it).
1831
+ let structuralPick:
1832
+ | {
1833
+ proposal: StructuralResonanceProposal;
1834
+ reach: AncestorReach;
1835
+ idf: number;
1033
1836
  }
1034
- // CONTRADICTION GUARD: a between-region already carrying its own
1035
- // vote must actually recur in this container's bytes — otherwise
1036
- // the container is a different learnt whole that happens to share
1037
- // ra/rb, and letting it stand in for the gap would silently
1038
- // override evidence the query itself already resolved there.
1837
+ | null = null;
1838
+ if (containers.length === 0) {
1839
+ // Structural-resonance composes each side's OWN gist directly (no
1840
+ // byte-containment truth backs it, unlike the DAG tiers) so, unlike
1841
+ // the DAG ladder (which tolerates one approximate side because byte
1842
+ // containment cannot lie), the ANN tier requires BOTH sides to be
1843
+ // KNOWN (content-addressed, exact identities): an approximate chunk
1844
+ // fragment's own resonance is noise at any tier, and composing noise
1845
+ // into a synthetic gist only manufactures a plausible-looking but
1846
+ // spurious ANN neighbour, not evidence of a genuine joint whole.
1847
+ // PHRASE-SCALE CONTRACT — the same one the DAG tiers hold their glue
1848
+ // to (see maxInterior above): a junction, exact or approximate, is a
1849
+ // whole the two forms nearly exhaust, not two arbitrary landmarks
1850
+ // anywhere in a long, multi-topic query. Without this, structural-
1851
+ // resonance would pair opposite ends of an unrelated scaffolding-
1852
+ // dominated query and manufacture a plausible-looking ANN neighbour
1853
+ // for a "gap" that never was a phrase.
1854
+ // BOTH sides must be independently DISCRIMINATIVE (individually
1855
+ // voted — `strong`, not merely a content-addressed `known` chunk):
1856
+ // a shared, non-discriminative scaffolding run (a repeated system
1857
+ // preamble) can be `known` without ever being distinctive evidence
1858
+ // of anything, and composing its own gist into a synthetic query
1859
+ // manufactures a plausible-looking but spurious ANN neighbour. The
1860
+ // DAG tiers can tolerate one merely-`known` side because byte
1861
+ // containment cannot lie; structural-resonance has no such
1862
+ // backstop, so both sides earn their place here the same way an
1863
+ // ordinary approximate region earns its individual vote.
1864
+ const gap = rb.start - ra.end;
1039
1865
  if (
1040
- between.some((bi) =>
1041
- indexOf(
1042
- bytes,
1043
- query.subarray(regions[bi].start, regions[bi].end),
1044
- 0,
1045
- ) < 0
1046
- )
1866
+ between.length === 0 &&
1867
+ strong.has(cand[a]) && strong.has(cand[b]) &&
1868
+ ra.known && rb.known &&
1869
+ gap <= maxInterior
1047
1870
  ) {
1048
- continue;
1871
+ const ownRootsA = rvs.votes.find((v) =>
1872
+ v.start === ra.start && v.end === ra.end
1873
+ )?.roots;
1874
+ const ownRootsB = rvs.votes.find((v) =>
1875
+ v.start === rb.start && v.end === rb.end
1876
+ )?.roots;
1877
+ structuralPick = await structuralResonance(
1878
+ ctx,
1879
+ query,
1880
+ ra,
1881
+ rb,
1882
+ sides,
1883
+ k,
1884
+ N,
1885
+ reachMemo,
1886
+ ownRootsA,
1887
+ ownRootsB,
1888
+ );
1049
1889
  }
1050
- let cov = left.length + right.length;
1051
- const extras: number[] = [];
1052
- for (const ei of cand) {
1053
- if (ei === cand[a] || ei === cand[b] || consumed.has(ei)) continue;
1054
- const e = regions[ei];
1055
- if (overlapsSpan(e, ra) || overlapsSpan(e, rb)) continue;
1056
- const eb = query.subarray(e.start, e.end);
1057
- if (indexOf(bytes, eb, 0) >= 0) {
1058
- extras.push(ei);
1059
- cov += eb.length;
1890
+ if (structuralPick === null) continue;
1891
+ tier = "structural-resonance";
1892
+ }
1893
+
1894
+ let best: (Junction | SynonymJunction) | null = null;
1895
+ let bestExtras: number[] = [];
1896
+ let bestCov = -1;
1897
+ let reach: AncestorReach;
1898
+ let idf: number;
1899
+ let confidence: number;
1900
+
1901
+ if (structuralPick !== null) {
1902
+ // A resonance proposal is NOT a Junction — there is no container to
1903
+ // read bytes from, so the self-evidence/contradiction/N-ary
1904
+ // machinery below (byte-verified against a real container) does not
1905
+ // apply; per spec §13, no N-ary extra-region coverage for resonance
1906
+ // proposals.
1907
+ best = { id: structuralPick.proposal.id, interior: new Uint8Array(0) };
1908
+ bestExtras = [];
1909
+ bestCov = rb.end - ra.start;
1910
+ reach = structuralPick.reach;
1911
+ idf = structuralPick.idf;
1912
+ confidence = structuralPick.proposal.effectiveScore;
1913
+ } else {
1914
+ // N-ARY selection: the container covering the MOST remaining candidate
1915
+ // forms wins (then tightest interior, then lowest id). Reads are
1916
+ // cache hits — every container's bytes were already read by the walk.
1917
+ //
1918
+ // SELF-EVIDENCE GUARD: a junction is BINDING evidence only when the
1919
+ // container joins forms the query mentions APART. When the container's
1920
+ // own joined occurrence (left..right including its interior) is a
1921
+ // literal substring of the query, the query already spells that phrase
1922
+ // out contiguously — perception already voted with it, and grid shards
1923
+ // of one phrase pairing "around" a gap chunk would merely rediscover
1924
+ // the phrase they are shards of, then explain away its rivals.
1925
+ for (const c of containers) {
1926
+ const bytes = cachedRead(ctx, cache, c.id, cap);
1927
+ const li = indexOf(bytes, left, 0);
1928
+ const ri = indexOf(bytes, right, 0);
1929
+ if (li >= 0 && ri >= 0) {
1930
+ const joined = bytes.subarray(
1931
+ Math.min(li, ri),
1932
+ Math.max(li + left.length, ri + right.length),
1933
+ );
1934
+ if (indexOf(query, joined, 0) >= 0) continue; // query says it itself
1935
+ }
1936
+ // CONTRADICTION GUARD: a between-region already carrying its own
1937
+ // vote must actually recur in this container's bytes — otherwise
1938
+ // the container is a different learnt whole that happens to share
1939
+ // ra/rb, and letting it stand in for the gap would silently
1940
+ // override evidence the query itself already resolved there.
1941
+ if (
1942
+ between.some((bi) =>
1943
+ indexOf(
1944
+ bytes,
1945
+ query.subarray(regions[bi].start, regions[bi].end),
1946
+ 0,
1947
+ ) < 0
1948
+ )
1949
+ ) {
1950
+ continue;
1951
+ }
1952
+ let cov = left.length + right.length;
1953
+ const extras: number[] = [];
1954
+ for (const ei of cand) {
1955
+ if (ei === cand[a] || ei === cand[b] || consumed.has(ei)) continue;
1956
+ const e = regions[ei];
1957
+ if (overlapsSpan(e, ra) || overlapsSpan(e, rb)) continue;
1958
+ const eb = query.subarray(e.start, e.end);
1959
+ if (indexOf(bytes, eb, 0) >= 0) {
1960
+ extras.push(ei);
1961
+ cov += eb.length;
1962
+ }
1963
+ }
1964
+ if (
1965
+ cov > bestCov ||
1966
+ (cov === bestCov && best !== null &&
1967
+ (c.interior.length < best.interior.length ||
1968
+ (c.interior.length === best.interior.length && c.id < best.id)))
1969
+ ) {
1970
+ best = c;
1971
+ bestExtras = extras;
1972
+ bestCov = cov;
1060
1973
  }
1061
1974
  }
1062
- if (
1063
- cov > bestCov ||
1064
- (cov === bestCov && best !== null &&
1065
- (c.interior.length < best.interior.length ||
1066
- (c.interior.length === best.interior.length && c.id < best.id)))
1067
- ) {
1068
- best = c;
1069
- bestExtras = extras;
1070
- bestCov = cov;
1071
- }
1072
- }
1073
- if (best === null) continue; // every container was self-evidence
1975
+ if (best === null) continue; // every container was self-evidence
1074
1976
 
1075
- const reach = edgeAncestors(ctx, best.id, N, reachMemo);
1076
- if (reach.saturated || reach.roots.length === 0) continue;
1077
- const idf = Math.log(N / Math.max(1, reach.contextsReached));
1078
- if (idf <= 0) continue;
1977
+ const r = edgeAncestors(ctx, best.id, N, reachMemo);
1978
+ if (r.saturated || r.roots.length === 0) continue;
1979
+ const df = Math.log(N / Math.max(1, r.contextsReached));
1980
+ if (df <= 0) continue;
1981
+ reach = r;
1982
+ idf = df;
1983
+ // Confidence used by voting (spec §13): exact junction = 1;
1984
+ // single/double-synonym = the sibling(s)' score(s), carried on the
1985
+ // SynonymJunction the ladder selected.
1986
+ confidence = "confidence" in best ? best.confidence : 1;
1987
+ }
1079
1988
 
1080
- // EXACT joint evidence (score = 1): the container literally contains
1081
- // every composed form. Mutual-explanation weight over their COMBINED
1082
- // byte length the same magnitude reading voteRegions uses, here with
1083
- // the estimator collapsed to certainty, so mutual = min(ratio, 1/ratio).
1989
+ // MUTUAL-EXPLANATION WEIGHT the same formula for every tier, with
1990
+ // `confidence` collapsed to certainty (1) for exact evidence: under
1991
+ // that collapse this is byte-for-byte the old exact-only formula
1992
+ // (min(1,ratio)·min(1,1/ratio)). For structural-resonance,
1993
+ // `confidence` is already annScore·semanticConfidence — never
1994
+ // multiplied a second time.
1084
1995
  const lenR = Math.max(1, bestCov);
1085
1996
  const ratio = Math.sqrt(
1086
1997
  Math.max(1, ctx.store.contentLen(best.id, lenR * ctx.store.D)) / lenR,
1087
1998
  );
1088
- const mutual = Math.min(1, ratio) * Math.min(1, 1 / ratio);
1999
+ const mutual = Math.min(1, confidence * ratio) *
2000
+ Math.min(1, confidence / ratio);
1089
2001
  const w = (mutual * idf) / reach.roots.length;
1090
2002
  let spanStart = ra.start;
1091
2003
  let spanEnd = rb.end;
@@ -1107,15 +2019,33 @@ async function crossRegionVotes(
1107
2019
  // for, not evidence lost — `absorbed` (RegionVote's breadth-accounting
1108
2020
  // field) must credit the junction with all of it, not just the ONE
1109
2021
  // pooled axiom it collapses to.
1110
- const containerBytes = cachedRead(ctx, cache, best.id, cap);
1111
- const jointRoots = new Set(reach.roots);
2022
+ // Only EXACT DAG evidence may explain away ordinary votes (spec §15).
2023
+ // Single-synonym, double-synonym, and structural-resonance may ADD
2024
+ // supporting evidence but never remove it: their evidence is itself
2025
+ // approximate (a sibling substitution, or an ANN guess), so treating
2026
+ // their byte-containment the way exact containment is treated would
2027
+ // let an approximation override a genuine, independently-voted region.
1112
2028
  let explainedAway = 0;
1113
- for (const rv of rvs.votes) {
1114
- if (rv.roots.some((r) => jointRoots.has(r))) continue;
1115
- const bytes = query.subarray(rv.start, rv.end);
1116
- if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
1117
- superseded.add(rv);
1118
- explainedAway++;
2029
+ // Exact set of ORIGINAL region indices this junction explained away —
2030
+ // recorded live as `superseded.add` fires (spec §3's explicit rule:
2031
+ // never inferred from `absorbed` afterward).
2032
+ const explainedAwayIndices: number[] = [];
2033
+ if (tier === "exact") {
2034
+ const containerBytes = cachedRead(ctx, cache, best.id, cap);
2035
+ const jointRoots = new Set(reach.roots);
2036
+ for (const rv of rvs.votes) {
2037
+ if (rv.roots.some((r) => jointRoots.has(r))) continue;
2038
+ const bytes = query.subarray(rv.start, rv.end);
2039
+ if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
2040
+ superseded.add(rv);
2041
+ explainedAway++;
2042
+ if (td) {
2043
+ const idx = regions.findIndex((r) =>
2044
+ r.start === rv.start && r.end === rv.end
2045
+ );
2046
+ if (idx >= 0) explainedAwayIndices.push(idx);
2047
+ }
2048
+ }
1119
2049
  }
1120
2050
  }
1121
2051
 
@@ -1128,11 +2058,44 @@ async function crossRegionVotes(
1128
2058
  wFocus: w,
1129
2059
  absorbed: 1 + explainedAway,
1130
2060
  });
2061
+ if (td) {
2062
+ td.crossRegionJunctionVotes.push({
2063
+ container: best.id,
2064
+ span: [spanStart, spanEnd],
2065
+ roots: [...reach.roots],
2066
+ sourceRegionIndices: [cand[a], cand[b], ...bestExtras],
2067
+ explainedAwayRegionIndices: explainedAwayIndices,
2068
+ absorbed: 1 + explainedAway,
2069
+ tier,
2070
+ });
2071
+ }
1131
2072
 
1132
2073
  const label = [cand[a], cand[b], ...bestExtras]
1133
2074
  .sort((x, y) => regions[x].start - regions[y].start)
1134
2075
  .map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
1135
2076
  .join(" ▸ ");
2077
+ const tierNote = tier === "exact"
2078
+ ? `junction node ${best.id}` +
2079
+ (best.interior.length === 0
2080
+ ? " (adjacent)"
2081
+ : ` (interior "${dec(best.interior)}")`) +
2082
+ ", by content-addressed ascent"
2083
+ : tier === "structural-resonance"
2084
+ ? `structurally-composed ANN proposal, node ${best.id} — the query ` +
2085
+ `structurally composed the endpoint regions, the real middle-` +
2086
+ `query structure, and the selected halo-sibling endpoint ` +
2087
+ `direction(s) (variant ${structuralPick!.proposal.variant}, ` +
2088
+ `annScore ${structuralPick!.proposal.annScore.toFixed(3)} × ` +
2089
+ `semanticConfidence ${
2090
+ structuralPick!.proposal.semanticConfidence.toFixed(3)
2091
+ } = effectiveScore ${
2092
+ structuralPick!.proposal.effectiveScore.toFixed(3)
2093
+ }); it did not concatenate endpoint bytes or rewrite the query`
2094
+ : `${tier} junction node ${best.id}` +
2095
+ (best.interior.length === 0
2096
+ ? " (adjacent)"
2097
+ : ` (interior "${dec(best.interior)}")`) +
2098
+ `, by halo-sibling DAG ascent (confidence ${confidence.toFixed(3)})`;
1136
2099
  ctx.trace?.step(
1137
2100
  "crossRegion",
1138
2101
  [{ text: label, role: "pair" }],
@@ -1141,11 +2104,7 @@ async function crossRegionVotes(
1141
2104
  node: r,
1142
2105
  role: "joint-context",
1143
2106
  })),
1144
- `${label} → junction node ${best.id}` +
1145
- (best.interior.length === 0
1146
- ? " (adjacent)"
1147
- : ` (interior "${dec(best.interior)}")`) +
1148
- ` → ${reach.roots.length} context(s), by content-addressed ascent` +
2107
+ `${label} → ${tierNote} ${reach.roots.length} context(s)` +
1149
2108
  (superseded.size > 0
1150
2109
  ? `; ${superseded.size} aliasing vote(s) explained away`
1151
2110
  : ""),
@@ -1154,15 +2113,25 @@ async function crossRegionVotes(
1154
2113
  }
1155
2114
  }
1156
2115
 
2116
+ if (td) td.supersededOrdinaryVotes = superseded.size;
1157
2117
  return { votes: out, superseded };
1158
2118
  }
1159
2119
 
2120
+ /** Emit the "climbConsensus" step — the human-readable note this always
2121
+ * produced, now paired (when `ctx.trace` and `cfg` are both present) with
2122
+ * the structured {@link ClimbConsensusData} payload on the SAME step's
2123
+ * `data` field. Every exit of {@link computeAttention} funnels through
2124
+ * here, so instrumentation and the existing rationale text can never drift
2125
+ * apart — see the instrumentation spec's §9 "every exit path". */
1160
2126
  export function traceAttention(
1161
2127
  ctx: MindContext,
1162
2128
  regions: ReadonlyArray<{ start: number; end: number }>,
1163
2129
  regionVoter: ReadonlyArray<{ id: number; score: number; w: number } | null>,
1164
2130
  roots: ReadonlyArray<Attention>,
1165
2131
  steps: ReadonlyArray<DerivationStep> = [],
2132
+ td?: TraceDraft,
2133
+ cfg?: ClimbConsensusCfg,
2134
+ ranked: ReadonlyArray<Attention> = roots,
1166
2135
  ): void {
1167
2136
  if (!ctx.trace) return;
1168
2137
  const voters: RationaleItem[] = [];
@@ -1177,6 +2146,49 @@ export function traceAttention(
1177
2146
  // The pooled-evidence decision, one DerivationStep per anchor — the same
1178
2147
  // shape {@link GraphSearch}'s own cover steps take (see traceDerivation).
1179
2148
  if (steps.length > 0) traceDerivation(ctx, steps);
2149
+
2150
+ const data: ClimbConsensusData | undefined = (td && cfg)
2151
+ ? {
2152
+ version: 1,
2153
+ cache: { hit: false, detailAvailable: true },
2154
+ config: {
2155
+ annK: cfg.k,
2156
+ crossRegionProbeLimit: cfg.k,
2157
+ mode: cfg.mode,
2158
+ ...(cfg.N !== undefined ? { corpusN: cfg.N } : {}),
2159
+ dimension: ctx.store.D,
2160
+ ...(cfg.N !== undefined ? { hubBound: hubBound(ctx) } : {}),
2161
+ estimatorNoise: estimatorNoise(ctx.store.D),
2162
+ ...(cfg.naturalBreak !== undefined
2163
+ ? { naturalBreak: cfg.naturalBreak }
2164
+ : {}),
2165
+ ...(cfg.consensusFloor !== undefined
2166
+ ? { consensusFloor: cfg.consensusFloor }
2167
+ : {}),
2168
+ },
2169
+ candidates: {
2170
+ perceived: cfg.perceivedCount,
2171
+ recognised: cfg.totalRegions - cfg.perceivedCount,
2172
+ total: cfg.totalRegions,
2173
+ },
2174
+ ...(td.regions.length > 0 ? { regions: td.regions } : {}),
2175
+ ...(cfg.reachMemo ? { reaches: serialiseReaches(cfg.reachMemo) } : {}),
2176
+ ...(td.crossRegionSummary
2177
+ ? {
2178
+ crossRegion: {
2179
+ ...td.crossRegionSummary,
2180
+ junctionVotes: td.crossRegionJunctionVotes,
2181
+ supersededOrdinaryVotes: td.supersededOrdinaryVotes,
2182
+ },
2183
+ }
2184
+ : {}),
2185
+ ...(td.saturation ? { saturation: td.saturation } : {}),
2186
+ ...(td.pooling ? { pooling: td.pooling } : {}),
2187
+ ...(td.anchors.length > 0 ? { anchors: td.anchors } : {}),
2188
+ result: { roots: [...roots], ranked: [...ranked] },
2189
+ }
2190
+ : undefined;
2191
+
1180
2192
  t.done(
1181
2193
  roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)),
1182
2194
  roots.length === 0
@@ -1188,5 +2200,6 @@ export function traceAttention(
1188
2200
  : `${voters.length} of ${regions.length} sub-regions voted; consensus ordered ${roots.length} INDEPENDENT points of attention (votes ${
1189
2201
  roots.map((r) => r.vote.toFixed(2)).join(", ")
1190
2202
  })`,
2203
+ data,
1191
2204
  );
1192
2205
  }