@hviana/sema 0.2.4 → 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 (142) hide show
  1. package/package.json +1 -1
  2. package/src/geometry.ts +52 -3
  3. package/src/mind/attention.ts +1469 -120
  4. package/src/mind/index.ts +16 -0
  5. package/src/mind/junction.ts +125 -16
  6. package/src/mind/mind.ts +15 -0
  7. package/src/mind/rationale.ts +12 -4
  8. package/src/mind/traverse.ts +75 -7
  9. package/src/mind/types.ts +25 -0
  10. package/test/51-structural-resonance-ladder.test.mjs +552 -0
  11. package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
  12. package/test/53-cross-region-probe-instrumentation.test.mjs +356 -0
  13. package/dist/example/demo.d.ts +0 -1
  14. package/dist/example/demo.js +0 -39
  15. package/dist/example/train_base.d.ts +0 -87
  16. package/dist/example/train_base.js +0 -2252
  17. package/dist/src/alphabet.d.ts +0 -7
  18. package/dist/src/alphabet.js +0 -33
  19. package/dist/src/alu/src/alu.d.ts +0 -185
  20. package/dist/src/alu/src/alu.js +0 -440
  21. package/dist/src/alu/src/expr.d.ts +0 -61
  22. package/dist/src/alu/src/expr.js +0 -318
  23. package/dist/src/alu/src/index.d.ts +0 -11
  24. package/dist/src/alu/src/index.js +0 -19
  25. package/dist/src/alu/src/kernel-arith.d.ts +0 -16
  26. package/dist/src/alu/src/kernel-arith.js +0 -264
  27. package/dist/src/alu/src/kernel-bits.d.ts +0 -19
  28. package/dist/src/alu/src/kernel-bits.js +0 -152
  29. package/dist/src/alu/src/kernel-logic.d.ts +0 -4
  30. package/dist/src/alu/src/kernel-logic.js +0 -60
  31. package/dist/src/alu/src/kernel-nd.d.ts +0 -3
  32. package/dist/src/alu/src/kernel-nd.js +0 -208
  33. package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
  34. package/dist/src/alu/src/kernel-numeric.js +0 -366
  35. package/dist/src/alu/src/operation.d.ts +0 -168
  36. package/dist/src/alu/src/operation.js +0 -189
  37. package/dist/src/alu/src/parser.d.ts +0 -221
  38. package/dist/src/alu/src/parser.js +0 -577
  39. package/dist/src/alu/src/resonance.d.ts +0 -55
  40. package/dist/src/alu/src/resonance.js +0 -126
  41. package/dist/src/alu/src/text.d.ts +0 -31
  42. package/dist/src/alu/src/text.js +0 -73
  43. package/dist/src/alu/src/value.d.ts +0 -109
  44. package/dist/src/alu/src/value.js +0 -300
  45. package/dist/src/alu/test/alu.test.d.ts +0 -1
  46. package/dist/src/alu/test/alu.test.js +0 -764
  47. package/dist/src/bytes.d.ts +0 -14
  48. package/dist/src/bytes.js +0 -59
  49. package/dist/src/canon.d.ts +0 -26
  50. package/dist/src/canon.js +0 -57
  51. package/dist/src/config.d.ts +0 -111
  52. package/dist/src/config.js +0 -91
  53. package/dist/src/derive/src/deduction.d.ts +0 -125
  54. package/dist/src/derive/src/deduction.js +0 -155
  55. package/dist/src/derive/src/index.d.ts +0 -7
  56. package/dist/src/derive/src/index.js +0 -11
  57. package/dist/src/derive/src/priority-queue.d.ts +0 -20
  58. package/dist/src/derive/src/priority-queue.js +0 -73
  59. package/dist/src/derive/src/rewrite.d.ts +0 -56
  60. package/dist/src/derive/src/rewrite.js +0 -100
  61. package/dist/src/derive/src/trie.d.ts +0 -90
  62. package/dist/src/derive/src/trie.js +0 -217
  63. package/dist/src/derive/test/derive.test.d.ts +0 -1
  64. package/dist/src/derive/test/derive.test.js +0 -122
  65. package/dist/src/extension.d.ts +0 -37
  66. package/dist/src/extension.js +0 -7
  67. package/dist/src/geometry.d.ts +0 -176
  68. package/dist/src/geometry.js +0 -546
  69. package/dist/src/index.d.ts +0 -16
  70. package/dist/src/index.js +0 -18
  71. package/dist/src/ingest-cache.d.ts +0 -41
  72. package/dist/src/ingest-cache.js +0 -161
  73. package/dist/src/mind/articulation.d.ts +0 -6
  74. package/dist/src/mind/articulation.js +0 -99
  75. package/dist/src/mind/attention.d.ts +0 -88
  76. package/dist/src/mind/attention.js +0 -997
  77. package/dist/src/mind/bridge.d.ts +0 -30
  78. package/dist/src/mind/bridge.js +0 -569
  79. package/dist/src/mind/canonical.d.ts +0 -29
  80. package/dist/src/mind/canonical.js +0 -88
  81. package/dist/src/mind/graph-search.d.ts +0 -285
  82. package/dist/src/mind/graph-search.js +0 -956
  83. package/dist/src/mind/index.d.ts +0 -5
  84. package/dist/src/mind/index.js +0 -5
  85. package/dist/src/mind/junction.d.ts +0 -95
  86. package/dist/src/mind/junction.js +0 -262
  87. package/dist/src/mind/learning.d.ts +0 -47
  88. package/dist/src/mind/learning.js +0 -248
  89. package/dist/src/mind/match.d.ts +0 -126
  90. package/dist/src/mind/match.js +0 -441
  91. package/dist/src/mind/mechanisms/alu.d.ts +0 -4
  92. package/dist/src/mind/mechanisms/alu.js +0 -36
  93. package/dist/src/mind/mechanisms/cast.d.ts +0 -89
  94. package/dist/src/mind/mechanisms/cast.js +0 -716
  95. package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
  96. package/dist/src/mind/mechanisms/confluence.js +0 -225
  97. package/dist/src/mind/mechanisms/cover.d.ts +0 -6
  98. package/dist/src/mind/mechanisms/cover.js +0 -203
  99. package/dist/src/mind/mechanisms/extraction.d.ts +0 -67
  100. package/dist/src/mind/mechanisms/extraction.js +0 -387
  101. package/dist/src/mind/mechanisms/recall.d.ts +0 -13
  102. package/dist/src/mind/mechanisms/recall.js +0 -325
  103. package/dist/src/mind/mind.d.ts +0 -274
  104. package/dist/src/mind/mind.js +0 -572
  105. package/dist/src/mind/pipeline-mechanism.d.ts +0 -142
  106. package/dist/src/mind/pipeline-mechanism.js +0 -213
  107. package/dist/src/mind/pipeline.d.ts +0 -20
  108. package/dist/src/mind/pipeline.js +0 -217
  109. package/dist/src/mind/primitives.d.ts +0 -70
  110. package/dist/src/mind/primitives.js +0 -287
  111. package/dist/src/mind/rationale.d.ts +0 -134
  112. package/dist/src/mind/rationale.js +0 -162
  113. package/dist/src/mind/reasoning.d.ts +0 -34
  114. package/dist/src/mind/reasoning.js +0 -240
  115. package/dist/src/mind/recognition.d.ts +0 -20
  116. package/dist/src/mind/recognition.js +0 -390
  117. package/dist/src/mind/resonance.d.ts +0 -23
  118. package/dist/src/mind/resonance.js +0 -0
  119. package/dist/src/mind/trace.d.ts +0 -15
  120. package/dist/src/mind/trace.js +0 -73
  121. package/dist/src/mind/traverse.d.ts +0 -113
  122. package/dist/src/mind/traverse.js +0 -504
  123. package/dist/src/mind/types.d.ts +0 -271
  124. package/dist/src/mind/types.js +0 -130
  125. package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
  126. package/dist/src/rabitq-ivf/src/database.js +0 -201
  127. package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
  128. package/dist/src/rabitq-ivf/src/index.js +0 -4
  129. package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
  130. package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
  131. package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
  132. package/dist/src/rabitq-ivf/src/prng.js +0 -36
  133. package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
  134. package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
  135. package/dist/src/sema.d.ts +0 -31
  136. package/dist/src/sema.js +0 -63
  137. package/dist/src/store-sqlite.d.ts +0 -184
  138. package/dist/src/store-sqlite.js +0 -942
  139. package/dist/src/store.d.ts +0 -663
  140. package/dist/src/store.js +0 -1618
  141. package/dist/src/vec.d.ts +0 -31
  142. package/dist/src/vec.js +0 -109
@@ -25,23 +25,405 @@ 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
+ /** 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
+
105
+ export interface ConsensusRegionTrace {
106
+ index: number;
107
+ source: "perceived" | "recognised";
108
+ span: [number, number];
109
+ chunk: boolean;
110
+ known: boolean;
111
+
112
+ canonicalId?: number;
113
+ canonicalUsable: boolean;
114
+ canonicalFailed: boolean;
115
+
116
+ annQueried: boolean;
117
+ annHitsReturned: number;
118
+ annHitsExamined: number;
119
+
120
+ selected?: {
121
+ source: "canonical" | "ann";
122
+ node: number;
123
+ rank?: number;
124
+ score: number;
125
+ fallback?: "orphan" | "saturated-tie";
126
+ };
127
+
128
+ reachNode?: number;
129
+ outcome: RegionOutcome;
130
+
131
+ idf?: number;
132
+ dfWeight?: number;
133
+
134
+ contrastiveMargin?: number;
135
+ contrastiveNoiseFloor?: number;
136
+ contrastiveRival?: ConsensusContrastiveRivalTrace;
137
+
138
+ mutualWeight?: number;
139
+ voteWeightPerRoot?: number;
140
+ focusWeightPerRoot?: number;
141
+
142
+ ordinaryVoteProduced: boolean;
143
+ superseded: boolean;
144
+ }
145
+
146
+ export interface ConsensusReachTrace {
147
+ node: number;
148
+ roots: number[];
149
+ contextsReached: number;
150
+ saturated: boolean;
151
+ saturation?: SaturationStop;
152
+ }
153
+
154
+ export type AnchorRejectionReason =
155
+ | "below-natural-break"
156
+ | "below-consensus-floor"
157
+ | "leading-saturation";
158
+
159
+ export interface ConsensusAnchorTrace {
160
+ anchor: number;
161
+ rank: number;
162
+
163
+ pooledVote: number;
164
+ idfVote: number;
165
+
166
+ candidateBreadth: number;
167
+ contributingVotes: number;
168
+ contributingEvidence: number;
169
+ breadth: number;
170
+ contributingSpans: Array<[number, number]>;
171
+ clusters: number;
172
+
173
+ commit: {
174
+ status: "root" | "overlap" | "rejected";
175
+ dominant: boolean;
176
+ passesNaturalBreak?: boolean;
177
+ passesConsensusFloor?: boolean;
178
+ pastLeadingSaturation?: boolean;
179
+ rejectionReasons: AnchorRejectionReason[];
180
+ };
181
+ }
182
+
183
+ export interface JunctionVoteTrace {
184
+ container: number;
185
+ span: [number, number];
186
+ roots: number[];
187
+ sourceRegionIndices: number[];
188
+ explainedAwayRegionIndices: number[];
189
+ absorbed: number;
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";
292
+ }
293
+
294
+ export interface ClimbConsensusData {
295
+ version: 1;
296
+
297
+ cache: {
298
+ hit: boolean;
299
+ detailAvailable: boolean;
300
+ };
301
+
302
+ config: {
303
+ annK: number;
304
+ crossRegionProbeLimit: number;
305
+ mode: DFMode;
306
+ corpusN?: number;
307
+ dimension?: number;
308
+ hubBound?: number;
309
+ estimatorNoise?: number;
310
+ naturalBreak?: number;
311
+ consensusFloor?: number;
312
+ };
313
+
314
+ candidates: {
315
+ perceived: number;
316
+ recognised: number;
317
+ total: number;
318
+ };
319
+
320
+ regions?: ConsensusRegionTrace[];
321
+ reaches?: ConsensusReachTrace[];
322
+
323
+ crossRegion?: {
324
+ eligibleRegions: number;
325
+ maximalRegions: number;
326
+ probeLimit: number;
327
+ probesAttempted: number;
328
+ junctionVotes: JunctionVoteTrace[];
329
+ supersededOrdinaryVotes: number;
330
+ probes: CrossRegionProbeTrace[];
331
+ stopReason: "insufficient-regions" | "probe-limit" | "pairs-exhausted";
332
+ };
333
+
334
+ saturation?: {
335
+ regionIntervals: Array<{ start: number; end: number }>;
336
+ hasLeading: boolean;
337
+ leadingEnd: number;
338
+ };
339
+
340
+ pooling?: {
341
+ inputVotes: number;
342
+ eligibleVotes: number;
343
+ saturationMaskedVotes: number;
344
+ };
345
+
346
+ anchors?: ConsensusAnchorTrace[];
347
+
348
+ result: AttentionRead;
349
+ }
350
+
351
+ /** The mutable collection buffers threaded through one traced consensus
352
+ * climb — allocated exactly once, in {@link computeAttention}, only when
353
+ * `ctx.trace` is set. Every field mirrors a `ClimbConsensusData` array/map,
354
+ * built incrementally as the pipeline runs so commit-time decisions (in
355
+ * particular) are recorded LIVE, not reconstructed afterward. */
356
+ interface TraceDraft {
357
+ perceivedCount: number;
358
+ regions: ConsensusRegionTrace[];
359
+ crossRegionJunctionVotes: JunctionVoteTrace[];
360
+ crossRegionSummary?: {
361
+ eligibleRegions: number;
362
+ maximalRegions: number;
363
+ probeLimit: number;
364
+ probesAttempted: number;
365
+ stopReason?: "insufficient-regions" | "probe-limit" | "pairs-exhausted";
366
+ };
367
+ crossRegionProbes: CrossRegionProbeTrace[];
368
+ supersededOrdinaryVotes: number;
369
+ saturation?: {
370
+ regionIntervals: Array<{ start: number; end: number }>;
371
+ hasLeading: boolean;
372
+ leadingEnd: number;
373
+ };
374
+ pooling?: {
375
+ inputVotes: number;
376
+ eligibleVotes: number;
377
+ saturationMaskedVotes: number;
378
+ };
379
+ anchors: ConsensusAnchorTrace[];
380
+ }
381
+
382
+ /** The config/corpus context {@link traceAttention} needs to fill in
383
+ * `ClimbConsensusData.config` and `.result` at whichever exit fires —
384
+ * threaded down from {@link computeAttention} rather than re-derived, so
385
+ * every emission point reports the SAME numbers the real climb used. */
386
+ interface ClimbConsensusCfg {
387
+ k: number;
388
+ mode: DFMode;
389
+ perceivedCount: number;
390
+ totalRegions: number;
391
+ N?: number;
392
+ reachMemo?: ReadonlyMap<number, AncestorReach>;
393
+ naturalBreak?: number;
394
+ consensusFloor?: number;
395
+ }
396
+
397
+ function newTraceDraft(perceivedCount: number): TraceDraft {
398
+ return {
399
+ perceivedCount,
400
+ regions: [],
401
+ crossRegionJunctionVotes: [],
402
+ crossRegionProbes: [],
403
+ supersededOrdinaryVotes: 0,
404
+ anchors: [],
405
+ };
406
+ }
407
+
408
+ /** Serialise the shared `reachMemo` into the plain, authoritative saturation
409
+ * profile (spec §5) — every distinct node any tier's `edgeAncestors` call
410
+ * climbed from during this response, in insertion (first-consulted) order. */
411
+ function serialiseReaches(
412
+ reachMemo: ReadonlyMap<number, AncestorReach>,
413
+ ): ConsensusReachTrace[] {
414
+ const out: ConsensusReachTrace[] = [];
415
+ for (const [node, r] of reachMemo) {
416
+ out.push({
417
+ node,
418
+ roots: [...r.roots],
419
+ contextsReached: r.contextsReached,
420
+ saturated: r.saturated,
421
+ ...(r.saturation ? { saturation: r.saturation } : {}),
422
+ });
423
+ }
424
+ return out;
425
+ }
426
+
45
427
  // ── Public entry points ───────────────────────────────────────────────────
46
428
 
47
429
  /** Climb the query's perceived byte regions up the structural DAG via
@@ -85,12 +467,27 @@ export async function climbAttentionAll(
85
467
  }
86
468
  const hit = byRead.get(modeKey);
87
469
  if (hit !== undefined) {
470
+ // Cache-hit exit (spec §9): the abbreviated payload shape — only what
471
+ // is actually stored in the cached AttentionRead is reported. No
472
+ // candidate, reach, saturation, pooling or anchor detail is fabricated
473
+ // (that per-region detail was never retained by the memo).
474
+ const data: ClimbConsensusData | undefined = ctx.trace
475
+ ? {
476
+ version: 1,
477
+ cache: { hit: true, detailAvailable: false },
478
+ config: { annK: k, crossRegionProbeLimit: k, mode },
479
+ candidates: { perceived: 0, recognised: 0, total: 0 },
480
+ result: hit,
481
+ }
482
+ : undefined;
88
483
  ctx.trace?.step(
89
484
  "climbConsensus",
90
485
  [rItem(query, "query")],
91
486
  hit.roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)),
92
487
  `(cached) consensus already computed for this query — ` +
93
488
  `${hit.roots.length} point(s) of attention`,
489
+ undefined,
490
+ data,
94
491
  );
95
492
  return hit;
96
493
  }
@@ -110,6 +507,7 @@ export async function computeAttention(
110
507
  mode: DFMode,
111
508
  ): Promise<AttentionRead> {
112
509
  const regions = collectRegions(ctx, query);
510
+ const perceivedCount = regions.length;
113
511
 
114
512
  // Recognised sites carry structural evidence that perceived sub-regions
115
513
  // miss: a word crossing a W-boundary is split into chunks whose partial
@@ -130,14 +528,30 @@ export async function computeAttention(
130
528
  });
131
529
  }
132
530
 
133
- if (regions.length === 0) return { roots: [], ranked: [] };
531
+ // The trace draft (spec §9): allocated ONLY when a trace was requested —
532
+ // every downstream consumer gates its own writes on `td?` / `if (td)`, so
533
+ // an untraced climb pays zero allocation for this instrumentation.
534
+ const td: TraceDraft | undefined = ctx.trace
535
+ ? newTraceDraft(perceivedCount)
536
+ : undefined;
537
+ const cfg0: ClimbConsensusCfg = {
538
+ k,
539
+ mode,
540
+ perceivedCount,
541
+ totalRegions: regions.length,
542
+ };
543
+
544
+ if (regions.length === 0) {
545
+ traceAttention(ctx, [], [], [], undefined, td, cfg0);
546
+ return { roots: [], ranked: [] };
547
+ }
134
548
 
135
549
  const N = corpusN(ctx);
136
550
  // One climb per distinct anchor for the WHOLE query: regions sharing a
137
551
  // chunk, and canonicalChunkId's prefix probes, all hit this memo instead of
138
552
  // re-reading the anchor's full edge fan-out from the store.
139
553
  const reachMemo = new Map<number, AncestorReach>();
140
- const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo);
554
+ const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo, td);
141
555
 
142
556
  // ── Cross-region: DIRECT region-to-region interaction ─────────────────
143
557
  // Two regions whose individual climbs land on DIFFERENT contexts leave
@@ -152,6 +566,7 @@ export async function computeAttention(
152
566
  k,
153
567
  N,
154
568
  reachMemo,
569
+ td,
155
570
  );
156
571
  // A vote SUPERSEDED by exact joint evidence (its bytes literally live
157
572
  // inside the joint container, yet it climbed elsewhere — grid aliasing)
@@ -162,16 +577,37 @@ export async function computeAttention(
162
577
  ...cross.votes,
163
578
  ]
164
579
  : rvs.votes;
580
+ // Mark, on the per-region trace, the source region of every superseded
581
+ // ordinary vote (spec §4's final rule) — an explicit pass over the exact
582
+ // set crossRegionVotes' explaining-away logic removed, never inferred
583
+ // from `absorbed`.
584
+ if (td && cross.superseded.size > 0) {
585
+ for (const rv of cross.superseded) {
586
+ const region = td.regions.find(
587
+ (r) => r.span[0] === rv.start && r.span[1] === rv.end,
588
+ );
589
+ if (region) region.superseded = true;
590
+ }
591
+ }
165
592
  // ──────────────────────────────────────────────────────────────────────
166
593
 
594
+ const cfg: ClimbConsensusCfg = { ...cfg0, N, reachMemo };
595
+
167
596
  if (allVotes.length === 0) {
168
- traceAttention(ctx, regions, rvs.voters, []);
597
+ traceAttention(ctx, regions, rvs.voters, [], undefined, td, cfg);
169
598
  return { roots: [], ranked: [] };
170
599
  }
171
600
 
172
601
  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);
602
+ if (td) {
603
+ td.saturation = {
604
+ regionIntervals: sat.intervals.map((iv) => ({ ...iv })),
605
+ hasLeading: sat.hasLeading,
606
+ leadingEnd: sat.leadingEnd,
607
+ };
608
+ }
609
+ const pooled = poolVotes(ctx, allVotes, sat, N, td);
610
+ return commitVotes(ctx, pooled, sat, regions, rvs.voters, N, td, cfg);
175
611
  }
176
612
 
177
613
  export function collectRegions(ctx: MindContext, query: Uint8Array): Region[] {
@@ -212,6 +648,7 @@ export async function voteRegions(
212
648
  mode: DFMode,
213
649
  N: number,
214
650
  reachMemo?: Map<number, AncestorReach>,
651
+ td?: TraceDraft,
215
652
  ): Promise<{
216
653
  votes: RegionVote[];
217
654
  saturated: boolean[];
@@ -224,6 +661,37 @@ export async function voteRegions(
224
661
 
225
662
  for (let ri = 0; ri < regions.length; ri++) {
226
663
  const { v, start, end, chunk, known } = regions[ri];
664
+ // Trace-only bookkeeping for this region — allocated only under `td`
665
+ // (i.e. only when ctx.trace is set); see ConsensusRegionTrace/
666
+ // RegionOutcome (spec §4). `examinedIds` tracks distinct ANN hits
667
+ // whose edgeAncestors reach was actually CONSULTED here (not merely
668
+ // returned by resonate) — the fallback/margin loops below add to it.
669
+ const examinedIds = td ? new Set<number>() : undefined;
670
+ let annQueried = false;
671
+ let fallbackKind: "orphan" | "saturated-tie" | undefined;
672
+ const recordRegion = (
673
+ outcome: RegionOutcome,
674
+ extra: Partial<ConsensusRegionTrace> = {},
675
+ ) => {
676
+ if (!td) return;
677
+ td.regions[ri] = {
678
+ index: ri,
679
+ source: ri < td.perceivedCount ? "perceived" : "recognised",
680
+ span: [start, end],
681
+ chunk,
682
+ known,
683
+ canonicalId: canonicalId ?? undefined,
684
+ canonicalUsable,
685
+ canonicalFailed,
686
+ annQueried,
687
+ annHitsReturned: hits ? hits.length : 0,
688
+ annHitsExamined: examinedIds ? examinedIds.size : 0,
689
+ outcome,
690
+ ordinaryVoteProduced: outcome === "voted",
691
+ superseded: false,
692
+ ...extra,
693
+ };
694
+ };
227
695
 
228
696
  // EXACT-FIRST: a chunk whose canonical anchor is content-addressed needs
229
697
  // no estimator — identity is exact, so its score is 1 BY DEFINITION (the
@@ -243,23 +711,35 @@ export async function voteRegions(
243
711
  (ctx.store.hasParents(canonicalId) ||
244
712
  ctx.store.hasContainers(canonicalId));
245
713
  let hits: readonly Hit[] | null = null;
246
- const ensureHits = async (): Promise<readonly Hit[]> =>
247
- hits ??= await ctx.store.resonate(v, k);
714
+ const ensureHits = async (): Promise<readonly Hit[]> => {
715
+ if (hits === null) {
716
+ hits = await ctx.store.resonate(v, k);
717
+ annQueried = true;
718
+ }
719
+ return hits;
720
+ };
248
721
 
249
722
  const canonicalFailed = chunk && canonicalId === null;
250
723
  let voterId: number;
251
724
  let score: number;
252
725
  let scoreId: number; // the node the score was measured against
726
+ let selectedSource: "canonical" | "ann";
253
727
  if (canonicalUsable) {
254
728
  voterId = canonicalId!;
255
729
  score = 1;
256
730
  scoreId = canonicalId!;
731
+ selectedSource = "canonical";
257
732
  } else {
258
733
  const h = await ensureHits();
259
- if (h.length === 0) continue;
734
+ if (h.length === 0) {
735
+ recordRegion("no-ann-hit");
736
+ continue;
737
+ }
260
738
  voterId = h[0].id;
261
739
  score = h[0].score;
262
740
  scoreId = h[0].id;
741
+ selectedSource = "ann";
742
+ examinedIds?.add(voterId);
263
743
  }
264
744
 
265
745
  let reach = edgeAncestors(ctx, voterId, N, reachMemo);
@@ -273,6 +753,7 @@ export async function voteRegions(
273
753
  for (const h of await ensureHits()) {
274
754
  if (h.id === voterId) continue;
275
755
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
756
+ examinedIds?.add(h.id);
276
757
  if (r2.saturated || r2.roots.length > 0) {
277
758
  ctx.trace?.step(
278
759
  "anchorFallback",
@@ -284,6 +765,8 @@ export async function voteRegions(
284
765
  voterId = h.id;
285
766
  score = h.score;
286
767
  scoreId = h.id;
768
+ selectedSource = "ann";
769
+ fallbackKind = "orphan";
287
770
  break;
288
771
  }
289
772
  }
@@ -307,6 +790,7 @@ export async function voteRegions(
307
790
  if (h.id === voterId) continue;
308
791
  if (h.score < score - band) break; // hits are nearest-first
309
792
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
793
+ examinedIds?.add(h.id);
310
794
  if (!r2.saturated && r2.roots.length > 0) {
311
795
  ctx.trace?.step(
312
796
  "anchorFallback",
@@ -318,20 +802,52 @@ export async function voteRegions(
318
802
  voterId = h.id;
319
803
  score = h.score;
320
804
  scoreId = h.id;
805
+ selectedSource = "ann";
806
+ fallbackKind = "saturated-tie";
321
807
  break;
322
808
  }
323
809
  }
324
810
  }
325
811
  regionSaturated[ri] = reach.saturated;
326
- if (reach.roots.length === 0) continue;
327
- if (reach.saturated) continue;
812
+ const selected: ConsensusRegionTrace["selected"] | undefined = !td
813
+ ? undefined
814
+ : (() => {
815
+ const rank: number | undefined = selectedSource === "ann"
816
+ ? (hits as readonly Hit[] | null)?.findIndex((h: Hit) =>
817
+ h.id === voterId
818
+ )
819
+ : undefined;
820
+ return {
821
+ source: selectedSource,
822
+ node: voterId,
823
+ score,
824
+ ...(rank !== undefined ? { rank } : {}),
825
+ ...(fallbackKind ? { fallback: fallbackKind } : {}),
826
+ };
827
+ })();
828
+ if (reach.roots.length === 0) {
829
+ recordRegion("no-structural-reach", { selected, reachNode: voterId });
830
+ continue;
831
+ }
832
+ if (reach.saturated) {
833
+ recordRegion("saturated-abstention", { selected, reachNode: voterId });
834
+ continue;
835
+ }
328
836
 
329
837
  // One IDF per region — dfWeight() and the focus weight used to compute
330
838
  // the same logarithm independently.
331
839
  const idf = Math.log(N / Math.max(1, reach.contextsReached));
332
840
  const df = Math.log(1 + reach.contextsReached);
333
841
  const wf = mode === "direct" ? df : mode === "combined" ? idf + df : idf;
334
- if (wf <= 0) continue;
842
+ if (wf <= 0) {
843
+ recordRegion("nonpositive-df-weight", {
844
+ selected,
845
+ reachNode: voterId,
846
+ idf,
847
+ dfWeight: wf,
848
+ });
849
+ continue;
850
+ }
335
851
 
336
852
  // CONTRASTIVE-MARGIN GATE — the compensation the linear (byte-proportional)
337
853
  // fold demands, applied to APPROXIMATE evidence only. Under the linear
@@ -356,17 +872,38 @@ export async function voteRegions(
356
872
  // (reordered / paraphrased queries) below the floor so they grounded
357
873
  // nothing. Gating at the noise floor keeps frame-echo suppression (a frame
358
874
  // region's margin ≈ 0 is gated out) without penalising honest evidence.
875
+ let contrastiveMargin: number | undefined;
876
+ let contrastiveRival: ConsensusContrastiveRivalTrace | undefined;
359
877
  if (!known) {
360
878
  let margin = score;
361
- 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];
362
882
  if (h.id === voterId) continue;
363
883
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
884
+ examinedIds?.add(h.id);
364
885
  if (r2.saturated || r2.roots.length === 0) continue; // concludes nothing
365
886
  if (sameRoots(r2.roots, reach.roots)) continue; // same conclusion
366
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
+ }
367
891
  break;
368
892
  }
369
- if (margin <= estimatorNoise(ctx.store.D)) continue;
893
+ contrastiveMargin = margin;
894
+ const noiseFloor = estimatorNoise(ctx.store.D);
895
+ if (margin <= noiseFloor) {
896
+ recordRegion("contrastive-margin-rejection", {
897
+ selected,
898
+ reachNode: voterId,
899
+ idf,
900
+ dfWeight: wf,
901
+ contrastiveMargin: margin,
902
+ contrastiveNoiseFloor: noiseFloor,
903
+ ...(contrastiveRival ? { contrastiveRival } : {}),
904
+ });
905
+ continue;
906
+ }
370
907
  }
371
908
 
372
909
  // MUTUAL-EXPLANATION WEIGHT (angle + magnitude). Under the linear fold
@@ -406,6 +943,22 @@ export async function voteRegions(
406
943
  if (ctx.trace) {
407
944
  regionVoter[ri] = { id: voterId, score, w: wf };
408
945
  }
946
+ recordRegion("voted", {
947
+ selected,
948
+ reachNode: voterId,
949
+ idf,
950
+ dfWeight: wf,
951
+ ...(contrastiveMargin !== undefined
952
+ ? {
953
+ contrastiveMargin,
954
+ contrastiveNoiseFloor: estimatorNoise(ctx.store.D),
955
+ ...(contrastiveRival ? { contrastiveRival } : {}),
956
+ }
957
+ : {}),
958
+ mutualWeight: mutual,
959
+ voteWeightPerRoot: w,
960
+ focusWeightPerRoot: wFocus,
961
+ });
409
962
  }
410
963
  return {
411
964
  votes: regionVotes,
@@ -431,6 +984,7 @@ export function poolVotes(
431
984
  regionVotes: readonly RegionVote[],
432
985
  sat: SaturationInfo,
433
986
  N: number,
987
+ td?: TraceDraft,
434
988
  ): {
435
989
  votes: Map<number, number>;
436
990
  votesIdf: Map<number, number>;
@@ -453,6 +1007,13 @@ export function poolVotes(
453
1007
  }
454
1008
  eligible.push(ri);
455
1009
  }
1010
+ if (td) {
1011
+ td.pooling = {
1012
+ inputVotes: regionVotes.length,
1013
+ eligibleVotes: eligible.length,
1014
+ saturationMaskedVotes: regionVotes.length - eligible.length,
1015
+ };
1016
+ }
456
1017
 
457
1018
  const key = (it: AItem) =>
458
1019
  it.kind === "region"
@@ -612,11 +1173,13 @@ export function commitVotes(
612
1173
  regions: readonly Region[],
613
1174
  regionVoter: ReadonlyArray<{ id: number; score: number; w: number } | null>,
614
1175
  N: number,
1176
+ td?: TraceDraft,
1177
+ cfg?: ClimbConsensusCfg,
615
1178
  ): AttentionRead {
616
1179
  const { votes, votesIdf, support, regionSupport, regionSpans, steps } =
617
1180
  pooled;
618
1181
  if (votes.size === 0) {
619
- traceAttention(ctx, regions, regionVoter, [], steps);
1182
+ traceAttention(ctx, regions, regionVoter, [], steps, td, cfg);
620
1183
  return { roots: [], ranked: [] };
621
1184
  }
622
1185
 
@@ -658,25 +1221,121 @@ export function commitVotes(
658
1221
  const floor = consensusFloor(N);
659
1222
  const placed: Attention[] = [];
660
1223
  const roots: Attention[] = [];
661
- for (const point of ranked) {
1224
+ const recordAnchor = (
1225
+ point: Attention,
1226
+ rank: number,
1227
+ status: "root" | "overlap" | "rejected",
1228
+ dominant: boolean,
1229
+ passesNaturalBreak: boolean | undefined,
1230
+ passesConsensusFloor: boolean | undefined,
1231
+ pastLeadingSaturation: boolean | undefined,
1232
+ rejectionReasons: AnchorRejectionReason[],
1233
+ ) => {
1234
+ if (!td) return;
1235
+ td.anchors.push({
1236
+ anchor: point.anchor,
1237
+ rank,
1238
+ pooledVote: point.vote,
1239
+ idfVote: votesIdf.get(point.anchor) ?? 0,
1240
+ candidateBreadth: regions.length,
1241
+ contributingVotes: regionSpans.get(point.anchor)?.length ?? 0,
1242
+ contributingEvidence: regionSupport.get(point.anchor) ?? 0,
1243
+ breadth: point.breadth,
1244
+ contributingSpans: regionSpans.get(point.anchor) ?? [],
1245
+ clusters: point.clusters,
1246
+ commit: {
1247
+ status,
1248
+ dominant,
1249
+ passesNaturalBreak,
1250
+ passesConsensusFloor,
1251
+ pastLeadingSaturation,
1252
+ rejectionReasons,
1253
+ },
1254
+ });
1255
+ };
1256
+ for (let rank = 0; rank < ranked.length; rank++) {
1257
+ const point = ranked[rank];
662
1258
  const absorbed = placed.some((p) => overlaps(point, p));
663
- if (!absorbed) {
1259
+ // Commit decisions are recorded LIVE, inside this loop, in the exact
1260
+ // shape the gates below apply them — never reconstructed afterward from
1261
+ // the final `roots` (spec §8's explicit requirement).
1262
+ let status: "root" | "overlap" | "rejected";
1263
+ let dominant = false;
1264
+ let passesNaturalBreak: boolean | undefined;
1265
+ let passesConsensusFloor: boolean | undefined;
1266
+ let pastLeadingSaturation: boolean | undefined;
1267
+ const rejectionReasons: AnchorRejectionReason[] = [];
1268
+ if (absorbed) {
1269
+ status = "overlap";
1270
+ } else {
664
1271
  const pastLeading = !sat.hasLeading ||
665
1272
  roots.length === 0 || point.start >= sat.leadingEnd;
1273
+ pastLeadingSaturation = pastLeading;
666
1274
  const vote = votesIdf.get(point.anchor) ?? 0;
667
- if (
668
- (roots.length === 0 || (vote >= rootCut && vote >= floor)) &&
669
- pastLeading
670
- ) {
1275
+ if (roots.length === 0) {
1276
+ // The first non-overlapping root is DOMINANT and bypasses the two
1277
+ // vote thresholds (it always grounds) — only the leading-saturation
1278
+ // gate still applies to it.
1279
+ dominant = true;
1280
+ if (pastLeading) {
1281
+ status = "root";
1282
+ } else {
1283
+ status = "rejected";
1284
+ rejectionReasons.push("leading-saturation");
1285
+ }
1286
+ } else {
1287
+ passesNaturalBreak = vote >= rootCut;
1288
+ passesConsensusFloor = vote >= floor;
1289
+ if (passesNaturalBreak && passesConsensusFloor && pastLeading) {
1290
+ status = "root";
1291
+ } else {
1292
+ status = "rejected";
1293
+ if (!passesNaturalBreak) rejectionReasons.push("below-natural-break");
1294
+ if (!passesConsensusFloor) {
1295
+ rejectionReasons.push("below-consensus-floor");
1296
+ }
1297
+ if (!pastLeading) rejectionReasons.push("leading-saturation");
1298
+ }
1299
+ }
1300
+ if (status === "root") {
671
1301
  roots.push(point);
672
1302
  } else {
1303
+ recordAnchor(
1304
+ point,
1305
+ rank,
1306
+ status,
1307
+ dominant,
1308
+ passesNaturalBreak,
1309
+ passesConsensusFloor,
1310
+ pastLeadingSaturation,
1311
+ rejectionReasons,
1312
+ );
673
1313
  continue;
674
1314
  }
675
1315
  }
1316
+ recordAnchor(
1317
+ point,
1318
+ rank,
1319
+ status,
1320
+ dominant,
1321
+ passesNaturalBreak,
1322
+ passesConsensusFloor,
1323
+ pastLeadingSaturation,
1324
+ rejectionReasons,
1325
+ );
676
1326
  placed.push(point);
677
1327
  }
678
1328
 
679
- traceAttention(ctx, regions, regionVoter, roots, steps);
1329
+ traceAttention(
1330
+ ctx,
1331
+ regions,
1332
+ regionVoter,
1333
+ roots,
1334
+ steps,
1335
+ td,
1336
+ cfg ? { ...cfg, naturalBreak: rootCut, consensusFloor: floor } : undefined,
1337
+ ranked,
1338
+ );
680
1339
  return { roots, ranked };
681
1340
  }
682
1341
 
@@ -843,6 +1502,331 @@ export function naturalBreak(votes: number[]): number {
843
1502
  // container does not hold (a genuine second topic) are untouched.
844
1503
  // ═══════════════════════════════════════════════════════════════════════════
845
1504
 
1505
+ // ── Structural-resonance — the FINAL approximate tier ──────────────────────
1506
+ //
1507
+ // Reached only when every DAG junction tier (exact, single-synonym, double-
1508
+ // synonym) found no container. Composes a hypothetical structural gist from
1509
+ // ALREADY-EXISTING structural vectors — the two endpoint regions' own gists
1510
+ // (or, per variant, a halo sibling's stored gist occupying the same slot)
1511
+ // plus the REAL middle-query structure between them — and asks the ANN index
1512
+ // what already-learnt whole resembles that composition. It never perceives
1513
+ // concatenated endpoint bytes and never fabricates a rewritten query string;
1514
+ // see {@link composeStructuralGist}.
1515
+
1516
+ export type CrossRegionTier =
1517
+ | "exact"
1518
+ | "single-synonym"
1519
+ | "double-synonym"
1520
+ | "structural-resonance";
1521
+
1522
+ export interface StructuralVariant {
1523
+ left: StructuralPart;
1524
+ right: StructuralPart;
1525
+ kind:
1526
+ | "exact-exact"
1527
+ | "left-synonym"
1528
+ | "right-synonym"
1529
+ | "double-synonym";
1530
+ semanticConfidence: number;
1531
+ leftSiblingId?: number;
1532
+ rightSiblingId?: number;
1533
+ }
1534
+
1535
+ export interface StructuralResonanceProposal {
1536
+ id: number;
1537
+ annScore: number;
1538
+ semanticConfidence: number;
1539
+ effectiveScore: number;
1540
+ variant: StructuralVariant["kind"];
1541
+ leftSiblingId?: number;
1542
+ rightSiblingId?: number;
1543
+ }
1544
+
1545
+ const VARIANT_KIND_ORDER: Record<StructuralVariant["kind"], number> = {
1546
+ "exact-exact": -1,
1547
+ "left-synonym": 0,
1548
+ "right-synonym": 1,
1549
+ "double-synonym": 2,
1550
+ };
1551
+
1552
+ /** A node's structural gist, read directly from its own stored bytes — the
1553
+ * repository's node → gist accessor: content is immutable and perception is
1554
+ * a pure function of bytes, so re-perceiving a node's full stored bytes
1555
+ * reproduces exactly the gist it was interned with. Never concatenates
1556
+ * the sibling's bytes with anything else. */
1557
+ function storedNodeGist(ctx: MindContext, id: number): Vec {
1558
+ return gistOf(ctx, read(ctx, id));
1559
+ }
1560
+
1561
+ /** A halo sibling's structural part, occupying the ORIGINAL query-region
1562
+ * slot length — the sibling replaces only the DIRECTION, never the query's
1563
+ * own bytes, position, or gap (see §6 of the spec this implements). */
1564
+ function siblingPart(
1565
+ ctx: MindContext,
1566
+ sibling: Hit,
1567
+ originalRegion: { start: number; end: number },
1568
+ ): StructuralPart {
1569
+ return {
1570
+ v: storedNodeGist(ctx, sibling.id),
1571
+ len: originalRegion.end - originalRegion.start,
1572
+ };
1573
+ }
1574
+
1575
+ /** Build, bound and order every mandatory structural variant (§7-8): the
1576
+ * exact/exact composition is always kept; up to `ctx.cfg.haloQueryK`
1577
+ * synonym variants (single- and double-synonym combined, one shared
1578
+ * budget) are appended, ordered by confidence, then kind, then sibling id. */
1579
+ export function buildStructuralVariants(
1580
+ ctx: MindContext,
1581
+ ra: Region,
1582
+ rb: Region,
1583
+ sides: JunctionSynonymSides,
1584
+ ): {
1585
+ variants: StructuralVariant[];
1586
+ exactLeft: StructuralPart;
1587
+ exactRight: StructuralPart;
1588
+ } {
1589
+ const exactLeft: StructuralPart = { v: ra.v, len: ra.end - ra.start };
1590
+ const exactRight: StructuralPart = { v: rb.v, len: rb.end - rb.start };
1591
+
1592
+ const synonymVariants: StructuralVariant[] = [];
1593
+ for (const ls of sides.leftSiblings) {
1594
+ synonymVariants.push({
1595
+ left: siblingPart(ctx, ls, ra),
1596
+ right: exactRight,
1597
+ kind: "left-synonym",
1598
+ semanticConfidence: ls.score,
1599
+ leftSiblingId: ls.id,
1600
+ });
1601
+ }
1602
+ for (const rs of sides.rightSiblings) {
1603
+ synonymVariants.push({
1604
+ left: exactLeft,
1605
+ right: siblingPart(ctx, rs, rb),
1606
+ kind: "right-synonym",
1607
+ semanticConfidence: rs.score,
1608
+ rightSiblingId: rs.id,
1609
+ });
1610
+ }
1611
+ for (const ls of sides.leftSiblings) {
1612
+ for (const rs of sides.rightSiblings) {
1613
+ synonymVariants.push({
1614
+ left: siblingPart(ctx, ls, ra),
1615
+ right: siblingPart(ctx, rs, rb),
1616
+ kind: "double-synonym",
1617
+ semanticConfidence: Math.min(ls.score, rs.score),
1618
+ leftSiblingId: ls.id,
1619
+ rightSiblingId: rs.id,
1620
+ });
1621
+ }
1622
+ }
1623
+
1624
+ // §8: semantic confidence desc, then kind (left-synonym, right-synonym,
1625
+ // double-synonym), then left sibling id asc, then right sibling id asc.
1626
+ synonymVariants.sort((a, b) =>
1627
+ b.semanticConfidence - a.semanticConfidence ||
1628
+ VARIANT_KIND_ORDER[a.kind] - VARIANT_KIND_ORDER[b.kind] ||
1629
+ (a.leftSiblingId ?? -1) - (b.leftSiblingId ?? -1) ||
1630
+ (a.rightSiblingId ?? -1) - (b.rightSiblingId ?? -1)
1631
+ );
1632
+
1633
+ const variants: StructuralVariant[] = [
1634
+ {
1635
+ left: exactLeft,
1636
+ right: exactRight,
1637
+ kind: "exact-exact",
1638
+ semanticConfidence: 1,
1639
+ },
1640
+ ...synonymVariants.slice(0, ctx.cfg.haloQueryK),
1641
+ ];
1642
+ return { variants, exactLeft, exactRight };
1643
+ }
1644
+
1645
+ /** Deterministic best-of tie-break for two proposals ranked for the SAME
1646
+ * candidate id — effectiveScore, then annScore, then semanticConfidence,
1647
+ * then variant kind, then sibling ids (§10). */
1648
+ function betterProposal(
1649
+ a: StructuralResonanceProposal,
1650
+ b: StructuralResonanceProposal,
1651
+ ): boolean {
1652
+ if (a.effectiveScore !== b.effectiveScore) {
1653
+ return a.effectiveScore > b.effectiveScore;
1654
+ }
1655
+ if (a.annScore !== b.annScore) return a.annScore > b.annScore;
1656
+ if (a.semanticConfidence !== b.semanticConfidence) {
1657
+ return a.semanticConfidence > b.semanticConfidence;
1658
+ }
1659
+ if (VARIANT_KIND_ORDER[a.variant] !== VARIANT_KIND_ORDER[b.variant]) {
1660
+ return VARIANT_KIND_ORDER[a.variant] < VARIANT_KIND_ORDER[b.variant];
1661
+ }
1662
+ if ((a.leftSiblingId ?? -1) !== (b.leftSiblingId ?? -1)) {
1663
+ return (a.leftSiblingId ?? -1) < (b.leftSiblingId ?? -1);
1664
+ }
1665
+ return (a.rightSiblingId ?? -1) < (b.rightSiblingId ?? -1);
1666
+ }
1667
+
1668
+ /** The final approximate tier: compose every retained structural variant,
1669
+ * ANN-query each, merge proposals by candidate id, and validate the winner
1670
+ * through the SAME structural gates every other tier answers to (saturation,
1671
+ * roots, IDF, contrastive margin). Returns null when nothing survives. */
1672
+ export async function structuralResonance(
1673
+ ctx: MindContext,
1674
+ query: Uint8Array,
1675
+ ra: Region,
1676
+ rb: Region,
1677
+ sides: JunctionSynonymSides,
1678
+ k: number,
1679
+ N: number,
1680
+ reachMemo: Map<number, AncestorReach>,
1681
+ /** Each side's OWN individual climb roots (from voteRegions), when it cast
1682
+ * one — the self-evidence backstop structural-resonance needs and the
1683
+ * exact tier gets for free from literal byte containment (§11's whole
1684
+ * premise: recover a JOINT context neither side votes for alone). A
1685
+ * candidate whose reach is exactly one side's own conclusion is not new
1686
+ * evidence of a joint whole; it is that side's resonance rediscovering
1687
+ * itself through a synthetic gist still dominated by its own direction. */
1688
+ ownRootsA: readonly number[] | undefined,
1689
+ ownRootsB: readonly number[] | undefined,
1690
+ trace?: StructuralResonanceTrace,
1691
+ ): Promise<
1692
+ | { proposal: StructuralResonanceProposal; reach: AncestorReach; idf: number }
1693
+ | null
1694
+ > {
1695
+ const { variants } = buildStructuralVariants(ctx, ra, rb, sides);
1696
+ if (trace) trace.variantBudget = ctx.cfg.haloQueryK;
1697
+
1698
+ const middleBytes = query.subarray(ra.end, rb.start);
1699
+ const middlePart: StructuralPart | null = middleBytes.length === 0
1700
+ ? null
1701
+ : { v: perceive(ctx, middleBytes).v, len: middleBytes.length };
1702
+
1703
+ const proposals = new Map<number, StructuralResonanceProposal>();
1704
+ for (const variant of variants) {
1705
+ const parts: StructuralPart[] = [variant.left];
1706
+ if (middlePart) parts.push(middlePart);
1707
+ parts.push(variant.right);
1708
+ const synthetic = composeStructuralGist(ctx.space, parts);
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
+ }
1719
+ for (const hit of hits) {
1720
+ const candidate: StructuralResonanceProposal = {
1721
+ id: hit.id,
1722
+ annScore: hit.score,
1723
+ semanticConfidence: variant.semanticConfidence,
1724
+ effectiveScore: hit.score * variant.semanticConfidence,
1725
+ variant: variant.kind,
1726
+ leftSiblingId: variant.leftSiblingId,
1727
+ rightSiblingId: variant.rightSiblingId,
1728
+ };
1729
+ const prev = proposals.get(hit.id);
1730
+ if (prev === undefined || betterProposal(candidate, prev)) {
1731
+ proposals.set(hit.id, candidate);
1732
+ }
1733
+ }
1734
+ }
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
+ }
1743
+
1744
+ const sorted = [...proposals.values()].sort((a, b) =>
1745
+ b.effectiveScore - a.effectiveScore || a.id - b.id
1746
+ );
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
+
1768
+ let selected: StructuralResonanceProposal | null = null;
1769
+ let selectedReach: AncestorReach | null = null;
1770
+ let selectedIdf = 0;
1771
+ let rival: StructuralResonanceProposal | null = null;
1772
+ for (const p of sorted) {
1773
+ const reach = edgeAncestors(ctx, p.id, N, reachMemo);
1774
+ if (reach.saturated || reach.roots.length === 0) {
1775
+ recordExamined(p, reach.saturated ? "saturated" : "no-roots");
1776
+ continue;
1777
+ }
1778
+ const idf = Math.log(N / Math.max(1, reach.contextsReached));
1779
+ if (idf <= 0) {
1780
+ recordExamined(p, "nonpositive-idf");
1781
+ continue;
1782
+ }
1783
+ // Self-evidence backstop (see the param doc above): a candidate that is
1784
+ // exactly one side's own already-voted conclusion carries no JOINT
1785
+ // evidence — skip it as if it never survived.
1786
+ if (
1787
+ (ownRootsA && sameRoots(reach.roots, ownRootsA)) ||
1788
+ (ownRootsB && sameRoots(reach.roots, ownRootsB))
1789
+ ) {
1790
+ recordExamined(p, "same-as-endpoint");
1791
+ continue;
1792
+ }
1793
+ if (selected === null) {
1794
+ selected = p;
1795
+ selectedReach = reach;
1796
+ selectedIdf = idf;
1797
+ recordExamined(p, "selected");
1798
+ } else if (!sameRoots(reach.roots, selectedReach!.roots)) {
1799
+ rival = p;
1800
+ recordExamined(p, "contrastive-rival");
1801
+ break;
1802
+ } else {
1803
+ recordExamined(p, "same-as-selected");
1804
+ }
1805
+ }
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
+ }
1813
+
1814
+ const margin = rival
1815
+ ? selected.effectiveScore - rival.effectiveScore
1816
+ : selected.effectiveScore;
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
+ }
1825
+
1826
+ if (trace) trace.outcome = "accepted";
1827
+ return { proposal: selected, reach: selectedReach, idf: selectedIdf };
1828
+ }
1829
+
846
1830
  async function crossRegionVotes(
847
1831
  ctx: MindContext,
848
1832
  query: Uint8Array,
@@ -851,6 +1835,7 @@ async function crossRegionVotes(
851
1835
  k: number,
852
1836
  N: number,
853
1837
  reachMemo: Map<number, AncestorReach>,
1838
+ td?: TraceDraft,
854
1839
  ): Promise<{ votes: RegionVote[]; superseded: Set<RegionVote> }> {
855
1840
  // Candidate regions: every region that ALREADY CAST ITS OWN VOTE in
856
1841
  // voteRegions — individually idf > 0, genuinely discriminative on its own,
@@ -902,6 +1887,15 @@ async function crossRegionVotes(
902
1887
  )
903
1888
  );
904
1889
  const none = { votes: [], superseded: new Set<RegionVote>() };
1890
+ if (td) {
1891
+ td.crossRegionSummary = {
1892
+ eligibleRegions: eligible.length,
1893
+ maximalRegions: cand.length,
1894
+ probeLimit: k,
1895
+ probesAttempted: 0, // updated below as probes accrue
1896
+ stopReason: cand.length < 2 ? "insufficient-regions" : undefined,
1897
+ };
1898
+ }
905
1899
  if (cand.length < 2) return none;
906
1900
  cand.sort((x, y) =>
907
1901
  regions[x].start - regions[y].start || regions[x].end - regions[y].end
@@ -972,6 +1966,34 @@ async function crossRegionVotes(
972
1966
  regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end)
973
1967
  ) continue;
974
1968
  probes++;
1969
+ if (td?.crossRegionSummary) {
1970
+ td.crossRegionSummary.probesAttempted = probes;
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
+ };
975
1997
 
976
1998
  const left = query.subarray(ra.start, ra.end);
977
1999
  const right = query.subarray(rb.start, rb.end);
@@ -980,112 +2002,322 @@ async function crossRegionVotes(
980
2002
  const maxInterior = (left.length + right.length) * ctx.space.maxGroup;
981
2003
  const cap = left.length + right.length + maxInterior;
982
2004
 
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
- );
2005
+ // The graded ladder (spec §1): exact DAG junction, then single-synonym,
2006
+ // then double-synonym, then only when every DAG tier found nothing —
2007
+ // structural-resonance. `sides` (the two halo sibling lists) is loaded
2008
+ // ONCE and reused by junctionSynonyms AND structural-resonance, so no
2009
+ // ladder rung repeats a halo ANN query an earlier rung already paid for.
2010
+ const sides = await loadJunctionSynonymSides(ctx, left, right);
2011
+
2012
+ let tier: CrossRegionTier = "exact";
2013
+ let containers: Array<Junction | SynonymJunction> =
2014
+ junctionContainersFrom(
2015
+ ctx,
2016
+ left,
2017
+ right,
2018
+ cap,
2019
+ seedsOf(cand[a]),
2020
+ seedsOf(cand[b]),
2021
+ undefined,
2022
+ true,
2023
+ );
2024
+ if (probe) {
2025
+ probe.exact = {
2026
+ attempted: true,
2027
+ candidatesReturned: containers.length,
2028
+ };
2029
+ }
997
2030
  if (containers.length === 0) {
998
- // Tier 2.5 — synonym containers (halo sibling of one side + the other).
999
- containers = await junctionSynonyms(
2031
+ // Tiers 2-4 — synonym containers (junctionSynonyms itself runs
2032
+ // single-synonym first, falling to double-synonym only when
2033
+ // single-synonym found nothing — see junction.ts).
2034
+ const syn = await junctionSynonyms(
1000
2035
  ctx,
1001
2036
  left,
1002
2037
  right,
1003
2038
  maxInterior,
1004
2039
  true,
2040
+ sides,
1005
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
+ }
2062
+ if (syn.length > 0) {
2063
+ containers = syn;
2064
+ tier = syn[0].tier;
2065
+ }
2066
+ }
2067
+
2068
+ // Tier 5 — structural-resonance ANN, the FINAL approximate proposal
2069
+ // path. Only reached when every DAG tier found NOTHING, and only when
2070
+ // there is no already-corroborated region between the endpoints (a
2071
+ // between-region with its own vote is evidence the gap already means
2072
+ // something specific — an ANN guess must not override it).
2073
+ let structuralPick:
2074
+ | {
2075
+ proposal: StructuralResonanceProposal;
2076
+ reach: AncestorReach;
2077
+ idf: number;
2078
+ }
2079
+ | null = null;
2080
+ if (containers.length === 0) {
2081
+ // Structural-resonance composes each side's OWN gist directly (no
2082
+ // byte-containment truth backs it, unlike the DAG tiers) — so, unlike
2083
+ // the DAG ladder (which tolerates one approximate side because byte
2084
+ // containment cannot lie), the ANN tier requires BOTH sides to be
2085
+ // KNOWN (content-addressed, exact identities): an approximate chunk
2086
+ // fragment's own resonance is noise at any tier, and composing noise
2087
+ // into a synthetic gist only manufactures a plausible-looking but
2088
+ // spurious ANN neighbour, not evidence of a genuine joint whole.
2089
+ // PHRASE-SCALE CONTRACT — the same one the DAG tiers hold their glue
2090
+ // to (see maxInterior above): a junction, exact or approximate, is a
2091
+ // whole the two forms nearly exhaust, not two arbitrary landmarks
2092
+ // anywhere in a long, multi-topic query. Without this, structural-
2093
+ // resonance would pair opposite ends of an unrelated scaffolding-
2094
+ // dominated query and manufacture a plausible-looking ANN neighbour
2095
+ // for a "gap" that never was a phrase.
2096
+ // BOTH sides must be independently DISCRIMINATIVE (individually
2097
+ // voted — `strong`, not merely a content-addressed `known` chunk):
2098
+ // a shared, non-discriminative scaffolding run (a repeated system
2099
+ // preamble) can be `known` without ever being distinctive evidence
2100
+ // of anything, and composing its own gist into a synthetic query
2101
+ // manufactures a plausible-looking but spurious ANN neighbour. The
2102
+ // DAG tiers can tolerate one merely-`known` side because byte
2103
+ // containment cannot lie; structural-resonance has no such
2104
+ // backstop, so both sides earn their place here the same way an
2105
+ // ordinary approximate region earns its individual vote.
2106
+ const gap = rb.start - ra.end;
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
+ }
2146
+ const ownRootsA = rvs.votes.find((v) =>
2147
+ v.start === ra.start && v.end === ra.end
2148
+ )?.roots;
2149
+ const ownRootsB = rvs.votes.find((v) =>
2150
+ v.start === rb.start && v.end === rb.end
2151
+ )?.roots;
2152
+ structuralPick = await structuralResonance(
2153
+ ctx,
2154
+ query,
2155
+ ra,
2156
+ rb,
2157
+ sides,
2158
+ k,
2159
+ N,
2160
+ reachMemo,
2161
+ ownRootsA,
2162
+ ownRootsB,
2163
+ resonanceTrace,
2164
+ );
2165
+ }
2166
+ if (structuralPick === null) {
2167
+ pushProbe(
2168
+ reasons.length > 0 ? "resonance-ineligible" : "resonance-rejected",
2169
+ );
2170
+ continue;
2171
+ }
2172
+ tier = "structural-resonance";
1006
2173
  }
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;
2174
+
2175
+ let best: (Junction | SynonymJunction) | null = null;
1021
2176
  let bestExtras: number[] = [];
1022
2177
  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
2178
+ let reach: AncestorReach;
2179
+ let idf: number;
2180
+ let confidence: number;
2181
+
2182
+ if (structuralPick !== null) {
2183
+ // A resonance proposal is NOT a Junction — there is no container to
2184
+ // read bytes from, so the self-evidence/contradiction/N-ary
2185
+ // machinery below (byte-verified against a real container) does not
2186
+ // apply; per spec §13, no N-ary extra-region coverage for resonance
2187
+ // proposals.
2188
+ best = { id: structuralPick.proposal.id, interior: new Uint8Array(0) };
2189
+ bestExtras = [];
2190
+ bestCov = rb.end - ra.start;
2191
+ reach = structuralPick.reach;
2192
+ idf = structuralPick.idf;
2193
+ confidence = structuralPick.proposal.effectiveScore;
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
+
2210
+ // N-ARY selection: the container covering the MOST remaining candidate
2211
+ // forms wins (then tightest interior, then lowest id). Reads are
2212
+ // cache hits — every container's bytes were already read by the walk.
2213
+ //
2214
+ // SELF-EVIDENCE GUARD: a junction is BINDING evidence only when the
2215
+ // container joins forms the query mentions APART. When the container's
2216
+ // own joined occurrence (left..right including its interior) is a
2217
+ // literal substring of the query, the query already spells that phrase
2218
+ // out contiguously — perception already voted with it, and grid shards
2219
+ // of one phrase pairing "around" a gap chunk would merely rediscover
2220
+ // the phrase they are shards of, then explain away its rivals.
2221
+ for (const c of containers) {
2222
+ const bytes = cachedRead(ctx, cache, c.id, cap);
2223
+ const li = indexOf(bytes, left, 0);
2224
+ const ri = indexOf(bytes, right, 0);
2225
+ if (li >= 0 && ri >= 0) {
2226
+ const joined = bytes.subarray(
2227
+ Math.min(li, ri),
2228
+ Math.max(li + left.length, ri + right.length),
2229
+ );
2230
+ if (indexOf(query, joined, 0) >= 0) {
2231
+ if (structuralTrace) structuralTrace.selfEvidenceRejected++;
2232
+ continue; // query says it itself
2233
+ }
2234
+ }
2235
+ // CONTRADICTION GUARD: a between-region already carrying its own
2236
+ // vote must actually recur in this container's bytes — otherwise
2237
+ // the container is a different learnt whole that happens to share
2238
+ // ra/rb, and letting it stand in for the gap would silently
2239
+ // override evidence the query itself already resolved there.
2240
+ if (
2241
+ between.some((bi) =>
2242
+ indexOf(
2243
+ bytes,
2244
+ query.subarray(regions[bi].start, regions[bi].end),
2245
+ 0,
2246
+ ) < 0
2247
+ )
2248
+ ) {
2249
+ if (structuralTrace) structuralTrace.contradictionRejected++;
2250
+ continue;
2251
+ }
2252
+ if (structuralTrace) structuralTrace.passedGuards++;
2253
+ let cov = left.length + right.length;
2254
+ const extras: number[] = [];
2255
+ for (const ei of cand) {
2256
+ if (ei === cand[a] || ei === cand[b] || consumed.has(ei)) continue;
2257
+ const e = regions[ei];
2258
+ if (overlapsSpan(e, ra) || overlapsSpan(e, rb)) continue;
2259
+ const eb = query.subarray(e.start, e.end);
2260
+ if (indexOf(bytes, eb, 0) >= 0) {
2261
+ extras.push(ei);
2262
+ cov += eb.length;
2263
+ }
2264
+ }
2265
+ if (
2266
+ cov > bestCov ||
2267
+ (cov === bestCov && best !== null &&
2268
+ (c.interior.length < best.interior.length ||
2269
+ (c.interior.length === best.interior.length && c.id < best.id)))
2270
+ ) {
2271
+ best = c;
2272
+ bestExtras = extras;
2273
+ bestCov = cov;
2274
+ }
1033
2275
  }
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.
1039
- if (
1040
- between.some((bi) =>
1041
- indexOf(
1042
- bytes,
1043
- query.subarray(regions[bi].start, regions[bi].end),
1044
- 0,
1045
- ) < 0
1046
- )
1047
- ) {
2276
+ if (best === null) {
2277
+ // every container was self-evidence / contradictionoutcome
2278
+ // stays "all-rejected".
2279
+ pushProbe("structural-rejected");
1048
2280
  continue;
1049
2281
  }
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;
2282
+
2283
+ const r = edgeAncestors(ctx, best.id, N, reachMemo);
2284
+ if (r.saturated || r.roots.length === 0) {
2285
+ if (structuralTrace) {
2286
+ structuralTrace.outcome = r.saturated ? "saturated" : "no-roots";
1060
2287
  }
2288
+ pushProbe("structural-rejected");
2289
+ continue;
1061
2290
  }
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;
2291
+ const df = Math.log(N / Math.max(1, r.contextsReached));
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;
1071
2300
  }
2301
+ reach = r;
2302
+ idf = df;
2303
+ // Confidence used by voting (spec §13): exact junction = 1;
2304
+ // single/double-synonym = the sibling(s)' score(s), carried on the
2305
+ // SynonymJunction the ladder selected.
2306
+ confidence = "confidence" in best ? best.confidence : 1;
1072
2307
  }
1073
- if (best === null) continue; // every container was self-evidence
1074
2308
 
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;
1079
-
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).
2309
+ // MUTUAL-EXPLANATION WEIGHT the same formula for every tier, with
2310
+ // `confidence` collapsed to certainty (1) for exact evidence: under
2311
+ // that collapse this is byte-for-byte the old exact-only formula
2312
+ // (min(1,ratio)·min(1,1/ratio)). For structural-resonance,
2313
+ // `confidence` is already annScore·semanticConfidence — never
2314
+ // multiplied a second time.
1084
2315
  const lenR = Math.max(1, bestCov);
1085
2316
  const ratio = Math.sqrt(
1086
2317
  Math.max(1, ctx.store.contentLen(best.id, lenR * ctx.store.D)) / lenR,
1087
2318
  );
1088
- const mutual = Math.min(1, ratio) * Math.min(1, 1 / ratio);
2319
+ const mutual = Math.min(1, confidence * ratio) *
2320
+ Math.min(1, confidence / ratio);
1089
2321
  const w = (mutual * idf) / reach.roots.length;
1090
2322
  let spanStart = ra.start;
1091
2323
  let spanEnd = rb.end;
@@ -1107,15 +2339,33 @@ async function crossRegionVotes(
1107
2339
  // for, not evidence lost — `absorbed` (RegionVote's breadth-accounting
1108
2340
  // field) must credit the junction with all of it, not just the ONE
1109
2341
  // pooled axiom it collapses to.
1110
- const containerBytes = cachedRead(ctx, cache, best.id, cap);
1111
- const jointRoots = new Set(reach.roots);
2342
+ // Only EXACT DAG evidence may explain away ordinary votes (spec §15).
2343
+ // Single-synonym, double-synonym, and structural-resonance may ADD
2344
+ // supporting evidence but never remove it: their evidence is itself
2345
+ // approximate (a sibling substitution, or an ANN guess), so treating
2346
+ // their byte-containment the way exact containment is treated would
2347
+ // let an approximation override a genuine, independently-voted region.
1112
2348
  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++;
2349
+ // Exact set of ORIGINAL region indices this junction explained away —
2350
+ // recorded live as `superseded.add` fires (spec §3's explicit rule:
2351
+ // never inferred from `absorbed` afterward).
2352
+ const explainedAwayIndices: number[] = [];
2353
+ if (tier === "exact") {
2354
+ const containerBytes = cachedRead(ctx, cache, best.id, cap);
2355
+ const jointRoots = new Set(reach.roots);
2356
+ for (const rv of rvs.votes) {
2357
+ if (rv.roots.some((r) => jointRoots.has(r))) continue;
2358
+ const bytes = query.subarray(rv.start, rv.end);
2359
+ if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
2360
+ superseded.add(rv);
2361
+ explainedAway++;
2362
+ if (td) {
2363
+ const idx = regions.findIndex((r) =>
2364
+ r.start === rv.start && r.end === rv.end
2365
+ );
2366
+ if (idx >= 0) explainedAwayIndices.push(idx);
2367
+ }
2368
+ }
1119
2369
  }
1120
2370
  }
1121
2371
 
@@ -1128,11 +2378,50 @@ async function crossRegionVotes(
1128
2378
  wFocus: w,
1129
2379
  absorbed: 1 + explainedAway,
1130
2380
  });
2381
+ pushProbe("accepted");
2382
+ if (td) {
2383
+ td.crossRegionJunctionVotes.push({
2384
+ container: best.id,
2385
+ span: [spanStart, spanEnd],
2386
+ roots: [...reach.roots],
2387
+ sourceRegionIndices: [cand[a], cand[b], ...bestExtras],
2388
+ explainedAwayRegionIndices: explainedAwayIndices,
2389
+ absorbed: 1 + explainedAway,
2390
+ tier,
2391
+ probe: td.crossRegionProbes.length - 1,
2392
+ confidence,
2393
+ evidenceBytes: bestCov,
2394
+ mutualWeight: mutual,
2395
+ voteWeightPerRoot: w,
2396
+ });
2397
+ }
1131
2398
 
1132
2399
  const label = [cand[a], cand[b], ...bestExtras]
1133
2400
  .sort((x, y) => regions[x].start - regions[y].start)
1134
2401
  .map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
1135
2402
  .join(" ▸ ");
2403
+ const tierNote = tier === "exact"
2404
+ ? `junction node ${best.id}` +
2405
+ (best.interior.length === 0
2406
+ ? " (adjacent)"
2407
+ : ` (interior "${dec(best.interior)}")`) +
2408
+ ", by content-addressed ascent"
2409
+ : tier === "structural-resonance"
2410
+ ? `structurally-composed ANN proposal, node ${best.id} — the query ` +
2411
+ `structurally composed the endpoint regions, the real middle-` +
2412
+ `query structure, and the selected halo-sibling endpoint ` +
2413
+ `direction(s) (variant ${structuralPick!.proposal.variant}, ` +
2414
+ `annScore ${structuralPick!.proposal.annScore.toFixed(3)} × ` +
2415
+ `semanticConfidence ${
2416
+ structuralPick!.proposal.semanticConfidence.toFixed(3)
2417
+ } = effectiveScore ${
2418
+ structuralPick!.proposal.effectiveScore.toFixed(3)
2419
+ }); it did not concatenate endpoint bytes or rewrite the query`
2420
+ : `${tier} junction node ${best.id}` +
2421
+ (best.interior.length === 0
2422
+ ? " (adjacent)"
2423
+ : ` (interior "${dec(best.interior)}")`) +
2424
+ `, by halo-sibling DAG ascent (confidence ${confidence.toFixed(3)})`;
1136
2425
  ctx.trace?.step(
1137
2426
  "crossRegion",
1138
2427
  [{ text: label, role: "pair" }],
@@ -1141,11 +2430,7 @@ async function crossRegionVotes(
1141
2430
  node: r,
1142
2431
  role: "joint-context",
1143
2432
  })),
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` +
2433
+ `${label} → ${tierNote} ${reach.roots.length} context(s)` +
1149
2434
  (superseded.size > 0
1150
2435
  ? `; ${superseded.size} aliasing vote(s) explained away`
1151
2436
  : ""),
@@ -1154,15 +2439,30 @@ async function crossRegionVotes(
1154
2439
  }
1155
2440
  }
1156
2441
 
2442
+ if (td) td.supersededOrdinaryVotes = superseded.size;
2443
+ if (td?.crossRegionSummary) {
2444
+ td.crossRegionSummary.stopReason = probes >= k
2445
+ ? "probe-limit"
2446
+ : "pairs-exhausted";
2447
+ }
1157
2448
  return { votes: out, superseded };
1158
2449
  }
1159
2450
 
2451
+ /** Emit the "climbConsensus" step — the human-readable note this always
2452
+ * produced, now paired (when `ctx.trace` and `cfg` are both present) with
2453
+ * the structured {@link ClimbConsensusData} payload on the SAME step's
2454
+ * `data` field. Every exit of {@link computeAttention} funnels through
2455
+ * here, so instrumentation and the existing rationale text can never drift
2456
+ * apart — see the instrumentation spec's §9 "every exit path". */
1160
2457
  export function traceAttention(
1161
2458
  ctx: MindContext,
1162
2459
  regions: ReadonlyArray<{ start: number; end: number }>,
1163
2460
  regionVoter: ReadonlyArray<{ id: number; score: number; w: number } | null>,
1164
2461
  roots: ReadonlyArray<Attention>,
1165
2462
  steps: ReadonlyArray<DerivationStep> = [],
2463
+ td?: TraceDraft,
2464
+ cfg?: ClimbConsensusCfg,
2465
+ ranked: ReadonlyArray<Attention> = roots,
1166
2466
  ): void {
1167
2467
  if (!ctx.trace) return;
1168
2468
  const voters: RationaleItem[] = [];
@@ -1177,6 +2477,54 @@ export function traceAttention(
1177
2477
  // The pooled-evidence decision, one DerivationStep per anchor — the same
1178
2478
  // shape {@link GraphSearch}'s own cover steps take (see traceDerivation).
1179
2479
  if (steps.length > 0) traceDerivation(ctx, steps);
2480
+
2481
+ const data: ClimbConsensusData | undefined = (td && cfg)
2482
+ ? {
2483
+ version: 1,
2484
+ cache: { hit: false, detailAvailable: true },
2485
+ config: {
2486
+ annK: cfg.k,
2487
+ crossRegionProbeLimit: cfg.k,
2488
+ mode: cfg.mode,
2489
+ ...(cfg.N !== undefined ? { corpusN: cfg.N } : {}),
2490
+ dimension: ctx.store.D,
2491
+ ...(cfg.N !== undefined ? { hubBound: hubBound(ctx) } : {}),
2492
+ estimatorNoise: estimatorNoise(ctx.store.D),
2493
+ ...(cfg.naturalBreak !== undefined
2494
+ ? { naturalBreak: cfg.naturalBreak }
2495
+ : {}),
2496
+ ...(cfg.consensusFloor !== undefined
2497
+ ? { consensusFloor: cfg.consensusFloor }
2498
+ : {}),
2499
+ },
2500
+ candidates: {
2501
+ perceived: cfg.perceivedCount,
2502
+ recognised: cfg.totalRegions - cfg.perceivedCount,
2503
+ total: cfg.totalRegions,
2504
+ },
2505
+ ...(td.regions.length > 0 ? { regions: td.regions } : {}),
2506
+ ...(cfg.reachMemo ? { reaches: serialiseReaches(cfg.reachMemo) } : {}),
2507
+ ...(td.crossRegionSummary
2508
+ ? {
2509
+ crossRegion: {
2510
+ eligibleRegions: td.crossRegionSummary.eligibleRegions,
2511
+ maximalRegions: td.crossRegionSummary.maximalRegions,
2512
+ probeLimit: td.crossRegionSummary.probeLimit,
2513
+ probesAttempted: td.crossRegionSummary.probesAttempted,
2514
+ junctionVotes: td.crossRegionJunctionVotes,
2515
+ supersededOrdinaryVotes: td.supersededOrdinaryVotes,
2516
+ probes: td.crossRegionProbes,
2517
+ stopReason: td.crossRegionSummary.stopReason ?? "pairs-exhausted",
2518
+ },
2519
+ }
2520
+ : {}),
2521
+ ...(td.saturation ? { saturation: td.saturation } : {}),
2522
+ ...(td.pooling ? { pooling: td.pooling } : {}),
2523
+ ...(td.anchors.length > 0 ? { anchors: td.anchors } : {}),
2524
+ result: { roots: [...roots], ranked: [...ranked] },
2525
+ }
2526
+ : undefined;
2527
+
1180
2528
  t.done(
1181
2529
  roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)),
1182
2530
  roots.length === 0
@@ -1188,5 +2536,6 @@ export function traceAttention(
1188
2536
  : `${voters.length} of ${regions.length} sub-regions voted; consensus ordered ${roots.length} INDEPENDENT points of attention (votes ${
1189
2537
  roots.map((r) => r.vote.toFixed(2)).join(", ")
1190
2538
  })`,
2539
+ data,
1191
2540
  );
1192
2541
  }