@hviana/sema 0.2.5 → 0.2.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 (133) hide show
  1. package/package.json +1 -1
  2. package/src/mind/attention.ts +355 -19
  3. package/test/53-cross-region-probe-instrumentation.test.mjs +356 -0
  4. package/dist/example/demo.d.ts +0 -1
  5. package/dist/example/demo.js +0 -39
  6. package/dist/example/train_base.d.ts +0 -87
  7. package/dist/example/train_base.js +0 -2252
  8. package/dist/src/alphabet.d.ts +0 -7
  9. package/dist/src/alphabet.js +0 -33
  10. package/dist/src/alu/src/alu.d.ts +0 -185
  11. package/dist/src/alu/src/alu.js +0 -440
  12. package/dist/src/alu/src/expr.d.ts +0 -61
  13. package/dist/src/alu/src/expr.js +0 -318
  14. package/dist/src/alu/src/index.d.ts +0 -11
  15. package/dist/src/alu/src/index.js +0 -19
  16. package/dist/src/alu/src/kernel-arith.d.ts +0 -16
  17. package/dist/src/alu/src/kernel-arith.js +0 -264
  18. package/dist/src/alu/src/kernel-bits.d.ts +0 -19
  19. package/dist/src/alu/src/kernel-bits.js +0 -152
  20. package/dist/src/alu/src/kernel-logic.d.ts +0 -4
  21. package/dist/src/alu/src/kernel-logic.js +0 -60
  22. package/dist/src/alu/src/kernel-nd.d.ts +0 -3
  23. package/dist/src/alu/src/kernel-nd.js +0 -208
  24. package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
  25. package/dist/src/alu/src/kernel-numeric.js +0 -366
  26. package/dist/src/alu/src/operation.d.ts +0 -168
  27. package/dist/src/alu/src/operation.js +0 -189
  28. package/dist/src/alu/src/parser.d.ts +0 -221
  29. package/dist/src/alu/src/parser.js +0 -577
  30. package/dist/src/alu/src/resonance.d.ts +0 -55
  31. package/dist/src/alu/src/resonance.js +0 -126
  32. package/dist/src/alu/src/text.d.ts +0 -31
  33. package/dist/src/alu/src/text.js +0 -73
  34. package/dist/src/alu/src/value.d.ts +0 -109
  35. package/dist/src/alu/src/value.js +0 -300
  36. package/dist/src/alu/test/alu.test.d.ts +0 -1
  37. package/dist/src/alu/test/alu.test.js +0 -764
  38. package/dist/src/bytes.d.ts +0 -14
  39. package/dist/src/bytes.js +0 -59
  40. package/dist/src/canon.d.ts +0 -26
  41. package/dist/src/canon.js +0 -57
  42. package/dist/src/config.d.ts +0 -111
  43. package/dist/src/config.js +0 -91
  44. package/dist/src/derive/src/deduction.d.ts +0 -125
  45. package/dist/src/derive/src/deduction.js +0 -155
  46. package/dist/src/derive/src/index.d.ts +0 -7
  47. package/dist/src/derive/src/index.js +0 -11
  48. package/dist/src/derive/src/priority-queue.d.ts +0 -20
  49. package/dist/src/derive/src/priority-queue.js +0 -73
  50. package/dist/src/derive/src/rewrite.d.ts +0 -56
  51. package/dist/src/derive/src/rewrite.js +0 -100
  52. package/dist/src/derive/src/trie.d.ts +0 -90
  53. package/dist/src/derive/src/trie.js +0 -217
  54. package/dist/src/derive/test/derive.test.d.ts +0 -1
  55. package/dist/src/derive/test/derive.test.js +0 -122
  56. package/dist/src/extension.d.ts +0 -37
  57. package/dist/src/extension.js +0 -7
  58. package/dist/src/geometry.d.ts +0 -202
  59. package/dist/src/geometry.js +0 -575
  60. package/dist/src/index.d.ts +0 -16
  61. package/dist/src/index.js +0 -18
  62. package/dist/src/ingest-cache.d.ts +0 -41
  63. package/dist/src/ingest-cache.js +0 -161
  64. package/dist/src/mind/articulation.d.ts +0 -6
  65. package/dist/src/mind/articulation.js +0 -99
  66. package/dist/src/mind/attention.d.ts +0 -315
  67. package/dist/src/mind/attention.js +0 -1549
  68. package/dist/src/mind/bridge.d.ts +0 -30
  69. package/dist/src/mind/bridge.js +0 -569
  70. package/dist/src/mind/canonical.d.ts +0 -29
  71. package/dist/src/mind/canonical.js +0 -88
  72. package/dist/src/mind/graph-search.d.ts +0 -285
  73. package/dist/src/mind/graph-search.js +0 -956
  74. package/dist/src/mind/index.d.ts +0 -7
  75. package/dist/src/mind/index.js +0 -5
  76. package/dist/src/mind/junction.d.ts +0 -124
  77. package/dist/src/mind/junction.js +0 -317
  78. package/dist/src/mind/learning.d.ts +0 -47
  79. package/dist/src/mind/learning.js +0 -248
  80. package/dist/src/mind/match.d.ts +0 -126
  81. package/dist/src/mind/match.js +0 -441
  82. package/dist/src/mind/mechanisms/alu.d.ts +0 -4
  83. package/dist/src/mind/mechanisms/alu.js +0 -36
  84. package/dist/src/mind/mechanisms/cast.d.ts +0 -89
  85. package/dist/src/mind/mechanisms/cast.js +0 -716
  86. package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
  87. package/dist/src/mind/mechanisms/confluence.js +0 -225
  88. package/dist/src/mind/mechanisms/cover.d.ts +0 -6
  89. package/dist/src/mind/mechanisms/cover.js +0 -203
  90. package/dist/src/mind/mechanisms/extraction.d.ts +0 -67
  91. package/dist/src/mind/mechanisms/extraction.js +0 -387
  92. package/dist/src/mind/mechanisms/recall.d.ts +0 -13
  93. package/dist/src/mind/mechanisms/recall.js +0 -325
  94. package/dist/src/mind/mind.d.ts +0 -276
  95. package/dist/src/mind/mind.js +0 -572
  96. package/dist/src/mind/pipeline-mechanism.d.ts +0 -142
  97. package/dist/src/mind/pipeline-mechanism.js +0 -213
  98. package/dist/src/mind/pipeline.d.ts +0 -20
  99. package/dist/src/mind/pipeline.js +0 -217
  100. package/dist/src/mind/primitives.d.ts +0 -70
  101. package/dist/src/mind/primitives.js +0 -287
  102. package/dist/src/mind/rationale.d.ts +0 -139
  103. package/dist/src/mind/rationale.js +0 -163
  104. package/dist/src/mind/reasoning.d.ts +0 -34
  105. package/dist/src/mind/reasoning.js +0 -240
  106. package/dist/src/mind/recognition.d.ts +0 -20
  107. package/dist/src/mind/recognition.js +0 -390
  108. package/dist/src/mind/resonance.d.ts +0 -23
  109. package/dist/src/mind/resonance.js +0 -0
  110. package/dist/src/mind/trace.d.ts +0 -15
  111. package/dist/src/mind/trace.js +0 -73
  112. package/dist/src/mind/traverse.d.ts +0 -113
  113. package/dist/src/mind/traverse.js +0 -568
  114. package/dist/src/mind/types.d.ts +0 -289
  115. package/dist/src/mind/types.js +0 -130
  116. package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
  117. package/dist/src/rabitq-ivf/src/database.js +0 -201
  118. package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
  119. package/dist/src/rabitq-ivf/src/index.js +0 -4
  120. package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
  121. package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
  122. package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
  123. package/dist/src/rabitq-ivf/src/prng.js +0 -36
  124. package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
  125. package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
  126. package/dist/src/sema.d.ts +0 -31
  127. package/dist/src/sema.js +0 -63
  128. package/dist/src/store-sqlite.d.ts +0 -184
  129. package/dist/src/store-sqlite.js +0 -942
  130. package/dist/src/store.d.ts +0 -663
  131. package/dist/src/store.js +0 -1618
  132. package/dist/src/vec.d.ts +0 -31
  133. package/dist/src/vec.js +0 -109
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -92,6 +92,16 @@ export type RegionOutcome =
92
92
  | "nonpositive-df-weight"
93
93
  | "contrastive-margin-rejection";
94
94
 
95
+ /** The best DIFFERENT-conclusion rival the contrastive-margin gate found
96
+ * while scanning an ordinary (approximate) region's ANN hits — spec §1.
97
+ * Its roots/saturation/contextsReached are already available through
98
+ * `reaches` (serialiseReaches) for `node`; not duplicated here. */
99
+ export interface ConsensusContrastiveRivalTrace {
100
+ node: number;
101
+ rank: number;
102
+ score: number;
103
+ }
104
+
95
105
  export interface ConsensusRegionTrace {
96
106
  index: number;
97
107
  source: "perceived" | "recognised";
@@ -123,6 +133,7 @@ export interface ConsensusRegionTrace {
123
133
 
124
134
  contrastiveMargin?: number;
125
135
  contrastiveNoiseFloor?: number;
136
+ contrastiveRival?: ConsensusContrastiveRivalTrace;
126
137
 
127
138
  mutualWeight?: number;
128
139
  voteWeightPerRoot?: number;
@@ -177,6 +188,107 @@ export interface JunctionVoteTrace {
177
188
  explainedAwayRegionIndices: number[];
178
189
  absorbed: number;
179
190
  tier?: ClimbConsensusJunctionTier;
191
+
192
+ /** Zero-based index into `crossRegion.probes` — the probe this vote was
193
+ * produced from (spec §8). */
194
+ probe: number;
195
+ confidence: number;
196
+ /** "Evidence bytes" — the container-coverage byte count (the existing
197
+ * `bestCov` variable at the push site). */
198
+ evidenceBytes: number;
199
+ mutualWeight: number;
200
+ voteWeightPerRoot: number;
201
+ }
202
+
203
+ /** Whether one DAG/synonym tier attempt was even made for a probe, and how
204
+ * many candidate containers it returned — spec §2/§3. */
205
+ export interface CrossRegionTierAttemptTrace {
206
+ attempted: boolean;
207
+ candidatesReturned: number;
208
+ }
209
+
210
+ /** Aggregate outcome of the container-selection loop for a DAG/synonym tier
211
+ * that returned at least one container — spec §4. Only aggregate counts
212
+ * and the final outcome are recorded, never every candidate. */
213
+ export interface CrossRegionStructuralTrace {
214
+ tier: "exact" | "single-synonym" | "double-synonym";
215
+ selfEvidenceRejected: number;
216
+ contradictionRejected: number;
217
+ passedGuards: number;
218
+ selectedNode?: number;
219
+ outcome:
220
+ | "all-rejected"
221
+ | "saturated"
222
+ | "no-roots"
223
+ | "nonpositive-idf"
224
+ | "accepted";
225
+ }
226
+
227
+ /** One retained structural-resonance variant that actually issued its own
228
+ * ANN query — spec §5. */
229
+ export interface StructuralResonanceVariantTrace {
230
+ kind: StructuralVariant["kind"];
231
+ semanticConfidence: number;
232
+ leftSiblingId?: number;
233
+ rightSiblingId?: number;
234
+ annHitsReturned: number;
235
+ }
236
+
237
+ /** One merged structural-resonance proposal actually examined via
238
+ * edgeAncestors — spec §5. Retains node/variant/scores, but NOT
239
+ * roots/saturation/contextsReached/idf (already in `reaches`). */
240
+ export interface StructuralResonanceCandidateTrace {
241
+ node: number;
242
+ variant: StructuralVariant["kind"];
243
+ leftSiblingId?: number;
244
+ rightSiblingId?: number;
245
+ annScore: number;
246
+ semanticConfidence: number;
247
+ effectiveScore: number;
248
+ outcome:
249
+ | "saturated"
250
+ | "no-roots"
251
+ | "nonpositive-idf"
252
+ | "same-as-endpoint"
253
+ | "same-as-selected"
254
+ | "selected"
255
+ | "contrastive-rival";
256
+ }
257
+
258
+ export interface StructuralResonanceTrace {
259
+ variantBudget: number;
260
+ variants: StructuralResonanceVariantTrace[];
261
+ mergedProposals: number;
262
+ examined: StructuralResonanceCandidateTrace[];
263
+ contrastiveMargin?: number;
264
+ noiseFloor: number;
265
+ outcome:
266
+ | "ineligible"
267
+ | "empty"
268
+ | "no-valid-proposal"
269
+ | "margin-rejected"
270
+ | "accepted";
271
+ ineligibleReasons?: Array<
272
+ "between-region" | "not-both-strong" | "not-both-known" | "gap-too-large"
273
+ >;
274
+ }
275
+
276
+ /** One cross-region pair the ladder actually probed — spec §2. Exactly one
277
+ * of these is pushed per pair that incremented `probes`. */
278
+ export interface CrossRegionProbeTrace {
279
+ leftRegionIndex: number;
280
+ rightRegionIndex: number;
281
+ betweenRegionIndices: number[];
282
+ exact: CrossRegionTierAttemptTrace;
283
+ singleSynonym: CrossRegionTierAttemptTrace;
284
+ doubleSynonym: CrossRegionTierAttemptTrace;
285
+ structural?: CrossRegionStructuralTrace;
286
+ resonance?: StructuralResonanceTrace;
287
+ outcome:
288
+ | "accepted"
289
+ | "structural-rejected"
290
+ | "resonance-ineligible"
291
+ | "resonance-rejected";
180
292
  }
181
293
 
182
294
  export interface ClimbConsensusData {
@@ -215,6 +327,8 @@ export interface ClimbConsensusData {
215
327
  probesAttempted: number;
216
328
  junctionVotes: JunctionVoteTrace[];
217
329
  supersededOrdinaryVotes: number;
330
+ probes: CrossRegionProbeTrace[];
331
+ stopReason: "insufficient-regions" | "probe-limit" | "pairs-exhausted";
218
332
  };
219
333
 
220
334
  saturation?: {
@@ -248,7 +362,9 @@ interface TraceDraft {
248
362
  maximalRegions: number;
249
363
  probeLimit: number;
250
364
  probesAttempted: number;
365
+ stopReason?: "insufficient-regions" | "probe-limit" | "pairs-exhausted";
251
366
  };
367
+ crossRegionProbes: CrossRegionProbeTrace[];
252
368
  supersededOrdinaryVotes: number;
253
369
  saturation?: {
254
370
  regionIntervals: Array<{ start: number; end: number }>;
@@ -283,6 +399,7 @@ function newTraceDraft(perceivedCount: number): TraceDraft {
283
399
  perceivedCount,
284
400
  regions: [],
285
401
  crossRegionJunctionVotes: [],
402
+ crossRegionProbes: [],
286
403
  supersededOrdinaryVotes: 0,
287
404
  anchors: [],
288
405
  };
@@ -756,15 +873,21 @@ export async function voteRegions(
756
873
  // nothing. Gating at the noise floor keeps frame-echo suppression (a frame
757
874
  // region's margin ≈ 0 is gated out) without penalising honest evidence.
758
875
  let contrastiveMargin: number | undefined;
876
+ let contrastiveRival: ConsensusContrastiveRivalTrace | undefined;
759
877
  if (!known) {
760
878
  let margin = score;
761
- for (const h of await ensureHits()) {
879
+ const hitsForRival = await ensureHits();
880
+ for (let hi = 0; hi < hitsForRival.length; hi++) {
881
+ const h = hitsForRival[hi];
762
882
  if (h.id === voterId) continue;
763
883
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
764
884
  examinedIds?.add(h.id);
765
885
  if (r2.saturated || r2.roots.length === 0) continue; // concludes nothing
766
886
  if (sameRoots(r2.roots, reach.roots)) continue; // same conclusion
767
887
  margin = score - h.score; // hits are nearest-first: the best rival
888
+ if (td) {
889
+ contrastiveRival = { node: h.id, rank: hi, score: h.score };
890
+ }
768
891
  break;
769
892
  }
770
893
  contrastiveMargin = margin;
@@ -777,6 +900,7 @@ export async function voteRegions(
777
900
  dfWeight: wf,
778
901
  contrastiveMargin: margin,
779
902
  contrastiveNoiseFloor: noiseFloor,
903
+ ...(contrastiveRival ? { contrastiveRival } : {}),
780
904
  });
781
905
  continue;
782
906
  }
@@ -828,6 +952,7 @@ export async function voteRegions(
828
952
  ? {
829
953
  contrastiveMargin,
830
954
  contrastiveNoiseFloor: estimatorNoise(ctx.store.D),
955
+ ...(contrastiveRival ? { contrastiveRival } : {}),
831
956
  }
832
957
  : {}),
833
958
  mutualWeight: mutual,
@@ -1562,11 +1687,13 @@ export async function structuralResonance(
1562
1687
  * itself through a synthetic gist still dominated by its own direction. */
1563
1688
  ownRootsA: readonly number[] | undefined,
1564
1689
  ownRootsB: readonly number[] | undefined,
1690
+ trace?: StructuralResonanceTrace,
1565
1691
  ): Promise<
1566
1692
  | { proposal: StructuralResonanceProposal; reach: AncestorReach; idf: number }
1567
1693
  | null
1568
1694
  > {
1569
1695
  const { variants } = buildStructuralVariants(ctx, ra, rb, sides);
1696
+ if (trace) trace.variantBudget = ctx.cfg.haloQueryK;
1570
1697
 
1571
1698
  const middleBytes = query.subarray(ra.end, rb.start);
1572
1699
  const middlePart: StructuralPart | null = middleBytes.length === 0
@@ -1580,6 +1707,15 @@ export async function structuralResonance(
1580
1707
  parts.push(variant.right);
1581
1708
  const synthetic = composeStructuralGist(ctx.space, parts);
1582
1709
  const hits = await ctx.store.resonate(synthetic, k);
1710
+ if (trace) {
1711
+ trace.variants.push({
1712
+ kind: variant.kind,
1713
+ semanticConfidence: variant.semanticConfidence,
1714
+ leftSiblingId: variant.leftSiblingId,
1715
+ rightSiblingId: variant.rightSiblingId,
1716
+ annHitsReturned: hits.length,
1717
+ });
1718
+ }
1583
1719
  for (const hit of hits) {
1584
1720
  const candidate: StructuralResonanceProposal = {
1585
1721
  id: hit.id,
@@ -1596,44 +1732,98 @@ export async function structuralResonance(
1596
1732
  }
1597
1733
  }
1598
1734
  }
1599
- if (proposals.size === 0) return null;
1735
+ if (trace) trace.mergedProposals = proposals.size;
1736
+ if (proposals.size === 0) {
1737
+ if (trace) {
1738
+ trace.noiseFloor = estimatorNoise(ctx.store.D);
1739
+ trace.outcome = "empty";
1740
+ }
1741
+ return null;
1742
+ }
1600
1743
 
1601
1744
  const sorted = [...proposals.values()].sort((a, b) =>
1602
1745
  b.effectiveScore - a.effectiveScore || a.id - b.id
1603
1746
  );
1604
1747
 
1748
+ // One shared shape for every `examined` entry (spec §5): only `outcome`
1749
+ // varies across the six exit points below, so build it once instead of
1750
+ // repeating the six-field literal at each site.
1751
+ const recordExamined = (
1752
+ p: StructuralResonanceProposal,
1753
+ outcome: StructuralResonanceCandidateTrace["outcome"],
1754
+ ) => {
1755
+ if (!trace) return;
1756
+ trace.examined.push({
1757
+ node: p.id,
1758
+ variant: p.variant,
1759
+ leftSiblingId: p.leftSiblingId,
1760
+ rightSiblingId: p.rightSiblingId,
1761
+ annScore: p.annScore,
1762
+ semanticConfidence: p.semanticConfidence,
1763
+ effectiveScore: p.effectiveScore,
1764
+ outcome,
1765
+ });
1766
+ };
1767
+
1605
1768
  let selected: StructuralResonanceProposal | null = null;
1606
1769
  let selectedReach: AncestorReach | null = null;
1607
1770
  let selectedIdf = 0;
1608
1771
  let rival: StructuralResonanceProposal | null = null;
1609
1772
  for (const p of sorted) {
1610
1773
  const reach = edgeAncestors(ctx, p.id, N, reachMemo);
1611
- if (reach.saturated || reach.roots.length === 0) continue;
1774
+ if (reach.saturated || reach.roots.length === 0) {
1775
+ recordExamined(p, reach.saturated ? "saturated" : "no-roots");
1776
+ continue;
1777
+ }
1612
1778
  const idf = Math.log(N / Math.max(1, reach.contextsReached));
1613
- if (idf <= 0) continue;
1779
+ if (idf <= 0) {
1780
+ recordExamined(p, "nonpositive-idf");
1781
+ continue;
1782
+ }
1614
1783
  // Self-evidence backstop (see the param doc above): a candidate that is
1615
1784
  // exactly one side's own already-voted conclusion carries no JOINT
1616
1785
  // evidence — skip it as if it never survived.
1617
1786
  if (
1618
1787
  (ownRootsA && sameRoots(reach.roots, ownRootsA)) ||
1619
1788
  (ownRootsB && sameRoots(reach.roots, ownRootsB))
1620
- ) continue;
1789
+ ) {
1790
+ recordExamined(p, "same-as-endpoint");
1791
+ continue;
1792
+ }
1621
1793
  if (selected === null) {
1622
1794
  selected = p;
1623
1795
  selectedReach = reach;
1624
1796
  selectedIdf = idf;
1797
+ recordExamined(p, "selected");
1625
1798
  } else if (!sameRoots(reach.roots, selectedReach!.roots)) {
1626
1799
  rival = p;
1800
+ recordExamined(p, "contrastive-rival");
1627
1801
  break;
1802
+ } else {
1803
+ recordExamined(p, "same-as-selected");
1628
1804
  }
1629
1805
  }
1630
- if (selected === null || selectedReach === null) return null;
1806
+ if (selected === null || selectedReach === null) {
1807
+ if (trace) {
1808
+ trace.noiseFloor = estimatorNoise(ctx.store.D);
1809
+ trace.outcome = "no-valid-proposal";
1810
+ }
1811
+ return null;
1812
+ }
1631
1813
 
1632
1814
  const margin = rival
1633
1815
  ? selected.effectiveScore - rival.effectiveScore
1634
1816
  : selected.effectiveScore;
1635
- if (margin <= estimatorNoise(ctx.store.D)) return null;
1817
+ if (trace) {
1818
+ trace.contrastiveMargin = margin;
1819
+ trace.noiseFloor = estimatorNoise(ctx.store.D);
1820
+ }
1821
+ if (margin <= estimatorNoise(ctx.store.D)) {
1822
+ if (trace) trace.outcome = "margin-rejected";
1823
+ return null;
1824
+ }
1636
1825
 
1826
+ if (trace) trace.outcome = "accepted";
1637
1827
  return { proposal: selected, reach: selectedReach, idf: selectedIdf };
1638
1828
  }
1639
1829
 
@@ -1703,6 +1893,7 @@ async function crossRegionVotes(
1703
1893
  maximalRegions: cand.length,
1704
1894
  probeLimit: k,
1705
1895
  probesAttempted: 0, // updated below as probes accrue
1896
+ stopReason: cand.length < 2 ? "insufficient-regions" : undefined,
1706
1897
  };
1707
1898
  }
1708
1899
  if (cand.length < 2) return none;
@@ -1778,6 +1969,31 @@ async function crossRegionVotes(
1778
1969
  if (td?.crossRegionSummary) {
1779
1970
  td.crossRegionSummary.probesAttempted = probes;
1780
1971
  }
1972
+ // Trace-only per-probe bookkeeping (spec §2-§7) — built incrementally
1973
+ // as the ladder runs, pushed exactly once at whichever exit fires
1974
+ // below. `pushProbe` is called at every continue/success exit for
1975
+ // THIS pair so the invariant `probes.length === probesAttempted`
1976
+ // holds regardless of which tier settled it.
1977
+ const probe: CrossRegionProbeTrace | undefined = td
1978
+ ? {
1979
+ leftRegionIndex: cand[a],
1980
+ rightRegionIndex: cand[b],
1981
+ betweenRegionIndices: [...between],
1982
+ exact: { attempted: false, candidatesReturned: 0 },
1983
+ singleSynonym: { attempted: false, candidatesReturned: 0 },
1984
+ doubleSynonym: { attempted: false, candidatesReturned: 0 },
1985
+ outcome: "structural-rejected",
1986
+ }
1987
+ : undefined;
1988
+ let probePushed = false;
1989
+ const pushProbe = (
1990
+ outcome: CrossRegionProbeTrace["outcome"],
1991
+ ) => {
1992
+ if (!td || !probe || probePushed) return;
1993
+ probe.outcome = outcome;
1994
+ td.crossRegionProbes.push(probe);
1995
+ probePushed = true;
1996
+ };
1781
1997
 
1782
1998
  const left = query.subarray(ra.start, ra.end);
1783
1999
  const right = query.subarray(rb.start, rb.end);
@@ -1805,6 +2021,12 @@ async function crossRegionVotes(
1805
2021
  undefined,
1806
2022
  true,
1807
2023
  );
2024
+ if (probe) {
2025
+ probe.exact = {
2026
+ attempted: true,
2027
+ candidatesReturned: containers.length,
2028
+ };
2029
+ }
1808
2030
  if (containers.length === 0) {
1809
2031
  // Tiers 2-4 — synonym containers (junctionSynonyms itself runs
1810
2032
  // single-synonym first, falling to double-synonym only when
@@ -1817,6 +2039,26 @@ async function crossRegionVotes(
1817
2039
  true,
1818
2040
  sides,
1819
2041
  );
2042
+ if (probe) {
2043
+ const singleAttempted = sides.leftSiblings.length > 0 ||
2044
+ sides.rightSiblings.length > 0;
2045
+ const singleReturned = syn[0]?.tier === "single-synonym"
2046
+ ? syn.length
2047
+ : 0;
2048
+ const doubleAttempted = singleAttempted && singleReturned === 0 &&
2049
+ sides.leftSiblings.length > 0 && sides.rightSiblings.length > 0;
2050
+ const doubleReturned = syn[0]?.tier === "double-synonym"
2051
+ ? syn.length
2052
+ : 0;
2053
+ probe.singleSynonym = {
2054
+ attempted: singleAttempted,
2055
+ candidatesReturned: singleReturned,
2056
+ };
2057
+ probe.doubleSynonym = {
2058
+ attempted: doubleAttempted,
2059
+ candidatesReturned: doubleReturned,
2060
+ };
2061
+ }
1820
2062
  if (syn.length > 0) {
1821
2063
  containers = syn;
1822
2064
  tier = syn[0].tier;
@@ -1862,12 +2104,45 @@ async function crossRegionVotes(
1862
2104
  // backstop, so both sides earn their place here the same way an
1863
2105
  // ordinary approximate region earns its individual vote.
1864
2106
  const gap = rb.start - ra.end;
1865
- if (
1866
- between.length === 0 &&
1867
- strong.has(cand[a]) && strong.has(cand[b]) &&
1868
- ra.known && rb.known &&
1869
- gap <= maxInterior
1870
- ) {
2107
+ const reasons: NonNullable<
2108
+ StructuralResonanceTrace["ineligibleReasons"]
2109
+ > = [];
2110
+ if (between.length > 0) reasons.push("between-region");
2111
+ if (!strong.has(cand[a]) || !strong.has(cand[b])) {
2112
+ reasons.push("not-both-strong");
2113
+ }
2114
+ if (!ra.known || !rb.known) reasons.push("not-both-known");
2115
+ if (gap > maxInterior) reasons.push("gap-too-large");
2116
+ let resonanceTrace: StructuralResonanceTrace | undefined;
2117
+ if (reasons.length > 0) {
2118
+ if (probe) {
2119
+ resonanceTrace = {
2120
+ variantBudget: ctx.cfg.haloQueryK,
2121
+ variants: [],
2122
+ mergedProposals: 0,
2123
+ examined: [],
2124
+ noiseFloor: estimatorNoise(ctx.store.D),
2125
+ outcome: "ineligible",
2126
+ ineligibleReasons: reasons,
2127
+ };
2128
+ probe.resonance = resonanceTrace;
2129
+ }
2130
+ } else {
2131
+ if (probe) {
2132
+ // `outcome`/`noiseFloor` are required fields with no natural
2133
+ // "unset" value; structuralResonance (called just below) always
2134
+ // overwrites both before returning, on every one of its exit
2135
+ // paths — these are never read in their initial form.
2136
+ resonanceTrace = {
2137
+ variantBudget: ctx.cfg.haloQueryK,
2138
+ variants: [],
2139
+ mergedProposals: 0,
2140
+ examined: [],
2141
+ noiseFloor: 0,
2142
+ outcome: "empty",
2143
+ };
2144
+ probe.resonance = resonanceTrace;
2145
+ }
1871
2146
  const ownRootsA = rvs.votes.find((v) =>
1872
2147
  v.start === ra.start && v.end === ra.end
1873
2148
  )?.roots;
@@ -1885,9 +2160,15 @@ async function crossRegionVotes(
1885
2160
  reachMemo,
1886
2161
  ownRootsA,
1887
2162
  ownRootsB,
2163
+ resonanceTrace,
2164
+ );
2165
+ }
2166
+ if (structuralPick === null) {
2167
+ pushProbe(
2168
+ reasons.length > 0 ? "resonance-ineligible" : "resonance-rejected",
1888
2169
  );
2170
+ continue;
1889
2171
  }
1890
- if (structuralPick === null) continue;
1891
2172
  tier = "structural-resonance";
1892
2173
  }
1893
2174
 
@@ -1911,6 +2192,21 @@ async function crossRegionVotes(
1911
2192
  idf = structuralPick.idf;
1912
2193
  confidence = structuralPick.proposal.effectiveScore;
1913
2194
  } else {
2195
+ // Aggregate structural-tier trace (spec §4) — one per DAG tier that
2196
+ // returned at least one container (exact, single-synonym or
2197
+ // double-synonym); only aggregate counts and the final outcome are
2198
+ // recorded, never every candidate.
2199
+ const structuralTrace: CrossRegionStructuralTrace | undefined = probe
2200
+ ? {
2201
+ tier: tier as "exact" | "single-synonym" | "double-synonym",
2202
+ selfEvidenceRejected: 0,
2203
+ contradictionRejected: 0,
2204
+ passedGuards: 0,
2205
+ outcome: "all-rejected",
2206
+ }
2207
+ : undefined;
2208
+ if (probe) probe.structural = structuralTrace;
2209
+
1914
2210
  // N-ARY selection: the container covering the MOST remaining candidate
1915
2211
  // forms wins (then tightest interior, then lowest id). Reads are
1916
2212
  // cache hits — every container's bytes were already read by the walk.
@@ -1931,7 +2227,10 @@ async function crossRegionVotes(
1931
2227
  Math.min(li, ri),
1932
2228
  Math.max(li + left.length, ri + right.length),
1933
2229
  );
1934
- if (indexOf(query, joined, 0) >= 0) continue; // query says it itself
2230
+ if (indexOf(query, joined, 0) >= 0) {
2231
+ if (structuralTrace) structuralTrace.selfEvidenceRejected++;
2232
+ continue; // query says it itself
2233
+ }
1935
2234
  }
1936
2235
  // CONTRADICTION GUARD: a between-region already carrying its own
1937
2236
  // vote must actually recur in this container's bytes — otherwise
@@ -1947,8 +2246,10 @@ async function crossRegionVotes(
1947
2246
  ) < 0
1948
2247
  )
1949
2248
  ) {
2249
+ if (structuralTrace) structuralTrace.contradictionRejected++;
1950
2250
  continue;
1951
2251
  }
2252
+ if (structuralTrace) structuralTrace.passedGuards++;
1952
2253
  let cov = left.length + right.length;
1953
2254
  const extras: number[] = [];
1954
2255
  for (const ei of cand) {
@@ -1972,12 +2273,31 @@ async function crossRegionVotes(
1972
2273
  bestCov = cov;
1973
2274
  }
1974
2275
  }
1975
- if (best === null) continue; // every container was self-evidence
2276
+ if (best === null) {
2277
+ // every container was self-evidence / contradiction — outcome
2278
+ // stays "all-rejected".
2279
+ pushProbe("structural-rejected");
2280
+ continue;
2281
+ }
1976
2282
 
1977
2283
  const r = edgeAncestors(ctx, best.id, N, reachMemo);
1978
- if (r.saturated || r.roots.length === 0) continue;
2284
+ if (r.saturated || r.roots.length === 0) {
2285
+ if (structuralTrace) {
2286
+ structuralTrace.outcome = r.saturated ? "saturated" : "no-roots";
2287
+ }
2288
+ pushProbe("structural-rejected");
2289
+ continue;
2290
+ }
1979
2291
  const df = Math.log(N / Math.max(1, r.contextsReached));
1980
- if (df <= 0) continue;
2292
+ if (df <= 0) {
2293
+ if (structuralTrace) structuralTrace.outcome = "nonpositive-idf";
2294
+ pushProbe("structural-rejected");
2295
+ continue;
2296
+ }
2297
+ if (structuralTrace) {
2298
+ structuralTrace.outcome = "accepted";
2299
+ structuralTrace.selectedNode = best.id;
2300
+ }
1981
2301
  reach = r;
1982
2302
  idf = df;
1983
2303
  // Confidence used by voting (spec §13): exact junction = 1;
@@ -2058,6 +2378,7 @@ async function crossRegionVotes(
2058
2378
  wFocus: w,
2059
2379
  absorbed: 1 + explainedAway,
2060
2380
  });
2381
+ pushProbe("accepted");
2061
2382
  if (td) {
2062
2383
  td.crossRegionJunctionVotes.push({
2063
2384
  container: best.id,
@@ -2067,6 +2388,11 @@ async function crossRegionVotes(
2067
2388
  explainedAwayRegionIndices: explainedAwayIndices,
2068
2389
  absorbed: 1 + explainedAway,
2069
2390
  tier,
2391
+ probe: td.crossRegionProbes.length - 1,
2392
+ confidence,
2393
+ evidenceBytes: bestCov,
2394
+ mutualWeight: mutual,
2395
+ voteWeightPerRoot: w,
2070
2396
  });
2071
2397
  }
2072
2398
 
@@ -2114,6 +2440,11 @@ async function crossRegionVotes(
2114
2440
  }
2115
2441
 
2116
2442
  if (td) td.supersededOrdinaryVotes = superseded.size;
2443
+ if (td?.crossRegionSummary) {
2444
+ td.crossRegionSummary.stopReason = probes >= k
2445
+ ? "probe-limit"
2446
+ : "pairs-exhausted";
2447
+ }
2117
2448
  return { votes: out, superseded };
2118
2449
  }
2119
2450
 
@@ -2176,9 +2507,14 @@ export function traceAttention(
2176
2507
  ...(td.crossRegionSummary
2177
2508
  ? {
2178
2509
  crossRegion: {
2179
- ...td.crossRegionSummary,
2510
+ eligibleRegions: td.crossRegionSummary.eligibleRegions,
2511
+ maximalRegions: td.crossRegionSummary.maximalRegions,
2512
+ probeLimit: td.crossRegionSummary.probeLimit,
2513
+ probesAttempted: td.crossRegionSummary.probesAttempted,
2180
2514
  junctionVotes: td.crossRegionJunctionVotes,
2181
2515
  supersededOrdinaryVotes: td.supersededOrdinaryVotes,
2516
+ probes: td.crossRegionProbes,
2517
+ stopReason: td.crossRegionSummary.stopReason ?? "pairs-exhausted",
2182
2518
  },
2183
2519
  }
2184
2520
  : {}),