@hviana/sema 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/src/geometry.d.ts +26 -0
  2. package/dist/src/geometry.js +32 -3
  3. package/dist/src/mind/attention.d.ts +246 -7
  4. package/dist/src/mind/attention.js +771 -173
  5. package/dist/src/mind/bridge.d.ts +30 -0
  6. package/dist/src/mind/bridge.js +569 -0
  7. package/dist/src/mind/index.d.ts +2 -0
  8. package/dist/src/mind/junction.d.ts +30 -1
  9. package/dist/src/mind/junction.js +73 -18
  10. package/dist/src/mind/match.d.ts +15 -2
  11. package/dist/src/mind/match.js +3 -8
  12. package/dist/src/mind/mechanisms/cast.d.ts +54 -0
  13. package/dist/src/mind/mechanisms/cast.js +268 -37
  14. package/dist/src/mind/mechanisms/cover.js +16 -31
  15. package/dist/src/mind/mechanisms/recall.js +66 -0
  16. package/dist/src/mind/mind.d.ts +2 -0
  17. package/dist/src/mind/rationale.d.ts +7 -2
  18. package/dist/src/mind/rationale.js +6 -5
  19. package/dist/src/mind/reasoning.d.ts +11 -0
  20. package/dist/src/mind/reasoning.js +58 -2
  21. package/dist/src/mind/recognition.js +127 -7
  22. package/dist/src/mind/traverse.js +90 -25
  23. package/dist/src/mind/types.d.ts +57 -2
  24. package/dist/src/mind/types.js +38 -7
  25. package/dist/src/store.d.ts +12 -3
  26. package/dist/src/store.js +9 -3
  27. package/package.json +1 -1
  28. package/src/geometry.ts +52 -3
  29. package/src/mind/attention.ts +1199 -128
  30. package/src/mind/bridge.ts +596 -0
  31. package/src/mind/index.ts +16 -0
  32. package/src/mind/junction.ts +125 -16
  33. package/src/mind/match.ts +19 -5
  34. package/src/mind/mechanisms/cast.ts +290 -38
  35. package/src/mind/mechanisms/cover.ts +23 -36
  36. package/src/mind/mechanisms/recall.ts +79 -0
  37. package/src/mind/mind.ts +15 -0
  38. package/src/mind/rationale.ts +12 -4
  39. package/src/mind/reasoning.ts +71 -2
  40. package/src/mind/recognition.ts +132 -7
  41. package/src/mind/traverse.ts +91 -24
  42. package/src/mind/types.ts +95 -6
  43. package/src/store.ts +19 -5
  44. package/test/36-already-answered-fusion.test.mjs +128 -0
  45. package/test/37-cluster-dispersion-fusion.test.mjs +190 -0
  46. package/test/38-reason-restate-guard.test.mjs +94 -0
  47. package/test/39-cast-restate-guard.test.mjs +102 -0
  48. package/test/40-choosenext-scale-guard.test.mjs +75 -0
  49. package/test/41-seatofnode-direction.test.mjs +85 -0
  50. package/test/42-recognise-trace-idempotence.test.mjs +106 -0
  51. package/test/43-cast-analog-seat.test.mjs +244 -0
  52. package/test/44-recognise-edge-whitespace.test.mjs +63 -0
  53. package/test/45-liftanswer-restated-trim.test.mjs +60 -0
  54. package/test/46-recognise-multibyte-edge.test.mjs +85 -0
  55. package/test/47-cast-comparison-coverage.test.mjs +134 -0
  56. package/test/48-recognise-turn-connective.test.mjs +125 -0
  57. package/test/49-natural-units-synonym-bridge.test.mjs +175 -0
  58. package/test/50-cast-analog-consensus-floor.test.mjs +179 -0
  59. package/test/51-structural-resonance-ladder.test.mjs +552 -0
  60. package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
@@ -8,14 +8,39 @@
8
8
  // attention for the rest of the pipeline to follow.
9
9
  import { isChunk } from "../sema.js";
10
10
  import { lightestDerivation, } from "../derive/src/index.js";
11
- import { consensusFloor, dominates, estimatorNoise } from "../geometry.js";
11
+ import { composeStructuralGist, consensusFloor, dominates, estimatorNoise, } from "../geometry.js";
12
12
  import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
13
13
  import { recognise } from "./recognition.js";
14
14
  import { leafIdRun } from "./canonical.js";
15
- import { corpusN, edgeAncestors } from "./traverse.js";
16
- import { cachedRead, junctionContainersFrom, junctionSeeds, junctionSynonyms, walkCache, } from "./junction.js";
15
+ import { corpusN, edgeAncestors, hubBound } from "./traverse.js";
16
+ import { cachedRead, junctionContainersFrom, junctionSeeds, junctionSynonyms, loadJunctionSynonymSides, walkCache, } from "./junction.js";
17
17
  import { indexOf } from "../bytes.js";
18
- import { rNode, traceDerivation } from "./trace.js";
18
+ import { rItem, rNode, traceDerivation } from "./trace.js";
19
+ function newTraceDraft(perceivedCount) {
20
+ return {
21
+ perceivedCount,
22
+ regions: [],
23
+ crossRegionJunctionVotes: [],
24
+ supersededOrdinaryVotes: 0,
25
+ anchors: [],
26
+ };
27
+ }
28
+ /** Serialise the shared `reachMemo` into the plain, authoritative saturation
29
+ * profile (spec §5) — every distinct node any tier's `edgeAncestors` call
30
+ * climbed from during this response, in insertion (first-consulted) order. */
31
+ function serialiseReaches(reachMemo) {
32
+ const out = [];
33
+ for (const [node, r] of reachMemo) {
34
+ out.push({
35
+ node,
36
+ roots: [...r.roots],
37
+ contextsReached: r.contextsReached,
38
+ saturated: r.saturated,
39
+ ...(r.saturation ? { saturation: r.saturation } : {}),
40
+ });
41
+ }
42
+ return out;
43
+ }
19
44
  // ── Public entry points ───────────────────────────────────────────────────
20
45
  /** Climb the query's perceived byte regions up the structural DAG via
21
46
  * resonance, pool the evidence, and return only the ROOT points of
@@ -24,12 +49,21 @@ export async function climbAttention(ctx, query, k, mode = "inverse") {
24
49
  return (await climbAttentionAll(ctx, query, k, mode)).roots;
25
50
  }
26
51
  /** Full read-out of one consensus climb: both the roots (dominant points of
27
- * attention) and the entire ranked list. Cached via ctx.climbMemo when
28
- * ctx.trace is null. */
52
+ * attention) and the entire ranked list. Cached via ctx.climbMemo, ALWAYS —
53
+ * see {@link recognise} for why this memo (and recognise()'s own) must
54
+ * never be skipped while tracing: computeAttention's collectRegions walks
55
+ * the query's perceived tree via the same foldTree whose subtree-resolution
56
+ * fast path makes a second call on identical bytes non-idempotent once
57
+ * ctx._resolvedSubtrees is warm (which a multi-turn conversation's shared
58
+ * prefix subtrees guarantee by the second turn). A cache hit still emits
59
+ * a trace step — abbreviated, since the full per-sub-region voting detail
60
+ * {@link traceAttention} builds isn't preserved by the cached read-out —
61
+ * so a traced response is never silently blacked out for a repeated
62
+ * query. */
29
63
  export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
30
64
  // Content-keyed memo — works for both single-turn respond() and multi-turn
31
- // respondTurn(). Skipped while tracing.
32
- if (ctx.climbMemo && !ctx.trace) {
65
+ // respondTurn().
66
+ if (ctx.climbMemo) {
33
67
  const contentKey = latin1Key(query);
34
68
  const modeKey = `${k}:${mode}`;
35
69
  let byRead = ctx.climbMemo.get(contentKey);
@@ -37,8 +71,24 @@ export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
37
71
  ctx.climbMemo.set(contentKey, byRead = new Map());
38
72
  }
39
73
  const hit = byRead.get(modeKey);
40
- if (hit !== undefined)
74
+ if (hit !== undefined) {
75
+ // Cache-hit exit (spec §9): the abbreviated payload shape — only what
76
+ // is actually stored in the cached AttentionRead is reported. No
77
+ // candidate, reach, saturation, pooling or anchor detail is fabricated
78
+ // (that per-region detail was never retained by the memo).
79
+ const data = ctx.trace
80
+ ? {
81
+ version: 1,
82
+ cache: { hit: true, detailAvailable: false },
83
+ config: { annK: k, crossRegionProbeLimit: k, mode },
84
+ candidates: { perceived: 0, recognised: 0, total: 0 },
85
+ result: hit,
86
+ }
87
+ : undefined;
88
+ ctx.trace?.step("climbConsensus", [rItem(query, "query")], hit.roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)), `(cached) consensus already computed for this query — ` +
89
+ `${hit.roots.length} point(s) of attention`, undefined, data);
41
90
  return hit;
91
+ }
42
92
  const read = await computeAttention(ctx, query, k, mode);
43
93
  byRead.set(modeKey, read);
44
94
  return read;
@@ -48,6 +98,7 @@ export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
48
98
  // ── Pipeline ──────────────────────────────────────────────────────────────
49
99
  export async function computeAttention(ctx, query, k, mode) {
50
100
  const regions = collectRegions(ctx, query);
101
+ const perceivedCount = regions.length;
51
102
  // Recognised sites carry structural evidence that perceived sub-regions
52
103
  // miss: a word crossing a W-boundary is split into chunks whose partial
53
104
  // gists may not resonate distinctively, but the SITE (content-addressed,
@@ -66,20 +117,34 @@ export async function computeAttention(ctx, query, k, mode) {
66
117
  known: true, // a recognised site IS a stored form
67
118
  });
68
119
  }
69
- if (regions.length === 0)
120
+ // The trace draft (spec §9): allocated ONLY when a trace was requested —
121
+ // every downstream consumer gates its own writes on `td?` / `if (td)`, so
122
+ // an untraced climb pays zero allocation for this instrumentation.
123
+ const td = ctx.trace
124
+ ? newTraceDraft(perceivedCount)
125
+ : undefined;
126
+ const cfg0 = {
127
+ k,
128
+ mode,
129
+ perceivedCount,
130
+ totalRegions: regions.length,
131
+ };
132
+ if (regions.length === 0) {
133
+ traceAttention(ctx, [], [], [], undefined, td, cfg0);
70
134
  return { roots: [], ranked: [] };
135
+ }
71
136
  const N = corpusN(ctx);
72
137
  // One climb per distinct anchor for the WHOLE query: regions sharing a
73
138
  // chunk, and canonicalChunkId's prefix probes, all hit this memo instead of
74
139
  // re-reading the anchor's full edge fan-out from the store.
75
140
  const reachMemo = new Map();
76
- const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo);
141
+ const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo, td);
77
142
  // ── Cross-region: DIRECT region-to-region interaction ─────────────────
78
143
  // Two regions whose individual climbs land on DIFFERENT contexts leave
79
144
  // their JOINT context — the learnt whole that contains BOTH — with no
80
145
  // vote. crossRegionVotes recovers it by the bridge's content-addressed
81
146
  // junction ascent (see the note above the function).
82
- const cross = await crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo);
147
+ const cross = await crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td);
83
148
  // A vote SUPERSEDED by exact joint evidence (its bytes literally live
84
149
  // inside the joint container, yet it climbed elsewhere — grid aliasing)
85
150
  // is dropped, not down-weighted: the joint container explains it away.
@@ -89,14 +154,33 @@ export async function computeAttention(ctx, query, k, mode) {
89
154
  ...cross.votes,
90
155
  ]
91
156
  : rvs.votes;
157
+ // Mark, on the per-region trace, the source region of every superseded
158
+ // ordinary vote (spec §4's final rule) — an explicit pass over the exact
159
+ // set crossRegionVotes' explaining-away logic removed, never inferred
160
+ // from `absorbed`.
161
+ if (td && cross.superseded.size > 0) {
162
+ for (const rv of cross.superseded) {
163
+ const region = td.regions.find((r) => r.span[0] === rv.start && r.span[1] === rv.end);
164
+ if (region)
165
+ region.superseded = true;
166
+ }
167
+ }
92
168
  // ──────────────────────────────────────────────────────────────────────
169
+ const cfg = { ...cfg0, N, reachMemo };
93
170
  if (allVotes.length === 0) {
94
- traceAttention(ctx, regions, rvs.voters, []);
171
+ traceAttention(ctx, regions, rvs.voters, [], undefined, td, cfg);
95
172
  return { roots: [], ranked: [] };
96
173
  }
97
174
  const sat = detectSaturated(ctx, regions, rvs.saturated);
98
- const pooled = poolVotes(ctx, allVotes, sat, N);
99
- return commitVotes(ctx, pooled, sat, regions, rvs.voters, N);
175
+ if (td) {
176
+ td.saturation = {
177
+ regionIntervals: sat.intervals.map((iv) => ({ ...iv })),
178
+ hasLeading: sat.hasLeading,
179
+ leadingEnd: sat.leadingEnd,
180
+ };
181
+ }
182
+ const pooled = poolVotes(ctx, allVotes, sat, N, td);
183
+ return commitVotes(ctx, pooled, sat, regions, rvs.voters, N, td, cfg);
100
184
  }
101
185
  export function collectRegions(ctx, query) {
102
186
  const regions = [];
@@ -128,12 +212,41 @@ export function collectRegions(ctx, query) {
128
212
  });
129
213
  return regions;
130
214
  }
131
- export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
215
+ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td) {
132
216
  const regionSaturated = new Array(regions.length).fill(false);
133
217
  const regionVotes = [];
134
218
  const regionVoter = ctx.trace ? regions.map(() => null) : [];
135
219
  for (let ri = 0; ri < regions.length; ri++) {
136
220
  const { v, start, end, chunk, known } = regions[ri];
221
+ // Trace-only bookkeeping for this region — allocated only under `td`
222
+ // (i.e. only when ctx.trace is set); see ConsensusRegionTrace/
223
+ // RegionOutcome (spec §4). `examinedIds` tracks distinct ANN hits
224
+ // whose edgeAncestors reach was actually CONSULTED here (not merely
225
+ // returned by resonate) — the fallback/margin loops below add to it.
226
+ const examinedIds = td ? new Set() : undefined;
227
+ let annQueried = false;
228
+ let fallbackKind;
229
+ const recordRegion = (outcome, extra = {}) => {
230
+ if (!td)
231
+ return;
232
+ td.regions[ri] = {
233
+ index: ri,
234
+ source: ri < td.perceivedCount ? "perceived" : "recognised",
235
+ span: [start, end],
236
+ chunk,
237
+ known,
238
+ canonicalId: canonicalId ?? undefined,
239
+ canonicalUsable,
240
+ canonicalFailed,
241
+ annQueried,
242
+ annHitsReturned: hits ? hits.length : 0,
243
+ annHitsExamined: examinedIds ? examinedIds.size : 0,
244
+ outcome,
245
+ ordinaryVoteProduced: outcome === "voted",
246
+ superseded: false,
247
+ ...extra,
248
+ };
249
+ };
137
250
  // EXACT-FIRST: a chunk whose canonical anchor is content-addressed needs
138
251
  // no estimator — identity is exact, so its score is 1 BY DEFINITION (the
139
252
  // estimated cosine of a form with itself, minus quantisation noise, and
@@ -152,23 +265,35 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
152
265
  (ctx.store.hasParents(canonicalId) ||
153
266
  ctx.store.hasContainers(canonicalId));
154
267
  let hits = null;
155
- const ensureHits = async () => hits ??= await ctx.store.resonate(v, k);
268
+ const ensureHits = async () => {
269
+ if (hits === null) {
270
+ hits = await ctx.store.resonate(v, k);
271
+ annQueried = true;
272
+ }
273
+ return hits;
274
+ };
156
275
  const canonicalFailed = chunk && canonicalId === null;
157
276
  let voterId;
158
277
  let score;
159
278
  let scoreId; // the node the score was measured against
279
+ let selectedSource;
160
280
  if (canonicalUsable) {
161
281
  voterId = canonicalId;
162
282
  score = 1;
163
283
  scoreId = canonicalId;
284
+ selectedSource = "canonical";
164
285
  }
165
286
  else {
166
287
  const h = await ensureHits();
167
- if (h.length === 0)
288
+ if (h.length === 0) {
289
+ recordRegion("no-ann-hit");
168
290
  continue;
291
+ }
169
292
  voterId = h[0].id;
170
293
  score = h[0].score;
171
294
  scoreId = h[0].id;
295
+ selectedSource = "ann";
296
+ examinedIds?.add(voterId);
172
297
  }
173
298
  let reach = edgeAncestors(ctx, voterId, N, reachMemo);
174
299
  // A region's vote must not die with the TOP hit: `hits[1..k]` were
@@ -182,12 +307,15 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
182
307
  if (h.id === voterId)
183
308
  continue;
184
309
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
310
+ examinedIds?.add(h.id);
185
311
  if (r2.saturated || r2.roots.length > 0) {
186
312
  ctx.trace?.step("anchorFallback", [rNode(ctx, voterId, "orphan-anchor", score)], [rNode(ctx, h.id, "anchor", h.score)], "the top-ranked anchor climbs to no context — a lower-ranked hit votes instead");
187
313
  reach = r2;
188
314
  voterId = h.id;
189
315
  score = h.score;
190
316
  scoreId = h.id;
317
+ selectedSource = "ann";
318
+ fallbackKind = "orphan";
191
319
  break;
192
320
  }
193
321
  }
@@ -214,28 +342,56 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
214
342
  if (h.score < score - band)
215
343
  break; // hits are nearest-first
216
344
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
345
+ examinedIds?.add(h.id);
217
346
  if (!r2.saturated && r2.roots.length > 0) {
218
347
  ctx.trace?.step("anchorFallback", [rNode(ctx, voterId, "saturated-anchor", score)], [rNode(ctx, h.id, "anchor", h.score)], "the top-ranked anchor is a saturated hub tied within estimator noise — the tied hit votes instead");
219
348
  reach = r2;
220
349
  voterId = h.id;
221
350
  score = h.score;
222
351
  scoreId = h.id;
352
+ selectedSource = "ann";
353
+ fallbackKind = "saturated-tie";
223
354
  break;
224
355
  }
225
356
  }
226
357
  }
227
358
  regionSaturated[ri] = reach.saturated;
228
- if (reach.roots.length === 0)
359
+ const selected = !td
360
+ ? undefined
361
+ : (() => {
362
+ const rank = selectedSource === "ann"
363
+ ? hits?.findIndex((h) => h.id === voterId)
364
+ : undefined;
365
+ return {
366
+ source: selectedSource,
367
+ node: voterId,
368
+ score,
369
+ ...(rank !== undefined ? { rank } : {}),
370
+ ...(fallbackKind ? { fallback: fallbackKind } : {}),
371
+ };
372
+ })();
373
+ if (reach.roots.length === 0) {
374
+ recordRegion("no-structural-reach", { selected, reachNode: voterId });
229
375
  continue;
230
- if (reach.saturated)
376
+ }
377
+ if (reach.saturated) {
378
+ recordRegion("saturated-abstention", { selected, reachNode: voterId });
231
379
  continue;
380
+ }
232
381
  // One IDF per region — dfWeight() and the focus weight used to compute
233
382
  // the same logarithm independently.
234
383
  const idf = Math.log(N / Math.max(1, reach.contextsReached));
235
384
  const df = Math.log(1 + reach.contextsReached);
236
385
  const wf = mode === "direct" ? df : mode === "combined" ? idf + df : idf;
237
- if (wf <= 0)
386
+ if (wf <= 0) {
387
+ recordRegion("nonpositive-df-weight", {
388
+ selected,
389
+ reachNode: voterId,
390
+ idf,
391
+ dfWeight: wf,
392
+ });
238
393
  continue;
394
+ }
239
395
  // CONTRASTIVE-MARGIN GATE — the compensation the linear (byte-proportional)
240
396
  // fold demands, applied to APPROXIMATE evidence only. Under the linear
241
397
  // fold a resonance score reads "fraction of aligned shared bytes", so a
@@ -259,12 +415,14 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
259
415
  // (reordered / paraphrased queries) below the floor so they grounded
260
416
  // nothing. Gating at the noise floor keeps frame-echo suppression (a frame
261
417
  // region's margin ≈ 0 is gated out) without penalising honest evidence.
418
+ let contrastiveMargin;
262
419
  if (!known) {
263
420
  let margin = score;
264
421
  for (const h of await ensureHits()) {
265
422
  if (h.id === voterId)
266
423
  continue;
267
424
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
425
+ examinedIds?.add(h.id);
268
426
  if (r2.saturated || r2.roots.length === 0)
269
427
  continue; // concludes nothing
270
428
  if (sameRoots(r2.roots, reach.roots))
@@ -272,8 +430,19 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
272
430
  margin = score - h.score; // hits are nearest-first: the best rival
273
431
  break;
274
432
  }
275
- if (margin <= estimatorNoise(ctx.store.D))
433
+ contrastiveMargin = margin;
434
+ const noiseFloor = estimatorNoise(ctx.store.D);
435
+ if (margin <= noiseFloor) {
436
+ recordRegion("contrastive-margin-rejection", {
437
+ selected,
438
+ reachNode: voterId,
439
+ idf,
440
+ dfWeight: wf,
441
+ contrastiveMargin: margin,
442
+ contrastiveNoiseFloor: noiseFloor,
443
+ });
276
444
  continue;
445
+ }
277
446
  }
278
447
  // MUTUAL-EXPLANATION WEIGHT (angle + magnitude). Under the linear fold
279
448
  // cos = shared/(‖r‖·‖h‖) with ‖·‖² = content bytes, so the old score²
@@ -310,6 +479,21 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
310
479
  if (ctx.trace) {
311
480
  regionVoter[ri] = { id: voterId, score, w: wf };
312
481
  }
482
+ recordRegion("voted", {
483
+ selected,
484
+ reachNode: voterId,
485
+ idf,
486
+ dfWeight: wf,
487
+ ...(contrastiveMargin !== undefined
488
+ ? {
489
+ contrastiveMargin,
490
+ contrastiveNoiseFloor: estimatorNoise(ctx.store.D),
491
+ }
492
+ : {}),
493
+ mutualWeight: mutual,
494
+ voteWeightPerRoot: w,
495
+ focusWeightPerRoot: wFocus,
496
+ });
313
497
  }
314
498
  return {
315
499
  votes: regionVotes,
@@ -329,7 +513,7 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
329
513
  * merely logs alongside it. `votesIdf`/`support` are the same two
330
514
  * read-outs {@link commitVotes} always gated on; only how they accumulate
331
515
  * changed. */
332
- export function poolVotes(ctx, regionVotes, sat, N) {
516
+ export function poolVotes(ctx, regionVotes, sat, N, td) {
333
517
  const eligible = [];
334
518
  for (let ri = 0; ri < regionVotes.length; ri++) {
335
519
  const rv = regionVotes[ri];
@@ -339,6 +523,13 @@ export function poolVotes(ctx, regionVotes, sat, N) {
339
523
  }
340
524
  eligible.push(ri);
341
525
  }
526
+ if (td) {
527
+ td.pooling = {
528
+ inputVotes: regionVotes.length,
529
+ eligibleVotes: eligible.length,
530
+ saturationMaskedVotes: regionVotes.length - eligible.length,
531
+ };
532
+ }
342
533
  const key = (it) => it.kind === "region"
343
534
  ? `r${it.ri}`
344
535
  : it.kind === "anchor"
@@ -401,6 +592,7 @@ export function poolVotes(ctx, regionVotes, sat, N) {
401
592
  const votesIdf = new Map();
402
593
  const support = new Map();
403
594
  const regionSupport = new Map();
595
+ const regionSpans = new Map();
404
596
  const steps = [];
405
597
  let order = 0;
406
598
  for (const pc of pool.values()) {
@@ -409,6 +601,7 @@ export function poolVotes(ctx, regionVotes, sat, N) {
409
601
  const premises = [];
410
602
  const seenRi = new Set();
411
603
  let breadthSum = 0;
604
+ const spans = [];
412
605
  for (const c of pc.contributions) {
413
606
  const p0 = c.premises[0].item;
414
607
  if (p0.kind !== "region" || seenRi.has(p0.ri))
@@ -417,8 +610,10 @@ export function poolVotes(ctx, regionVotes, sat, N) {
417
610
  const rv = regionVotes[p0.ri];
418
611
  breadthSum += rv.absorbed ?? 1;
419
612
  premises.push({ kind: "form", span: [rv.start, rv.end] });
613
+ spans.push([rv.start, rv.end]);
420
614
  }
421
615
  regionSupport.set(pc.item.id, breadthSum);
616
+ regionSpans.set(pc.item.id, spans);
422
617
  steps.push({
423
618
  order: order++,
424
619
  move: "pool-vote",
@@ -448,12 +643,41 @@ export function poolVotes(ctx, regionVotes, sat, N) {
448
643
  }
449
644
  }
450
645
  }
451
- return { votes, votesIdf, support, regionSupport, steps };
646
+ return { votes, votesIdf, support, regionSupport, regionSpans, steps };
452
647
  }
453
- export function commitVotes(ctx, pooled, sat, regions, regionVoter, N) {
454
- const { votes, votesIdf, support, regionSupport, steps } = pooled;
648
+ /** The number of DISTINCT clusters a root's contributing regions form
649
+ * see Attention.clusters. Two regions belong to the same cluster iff the
650
+ * gap between them is strictly less than one river-fold quantum W: at
651
+ * that distance there is no room for a genuinely separate, independently
652
+ * perceivable unit of content between them (the same "smallest meaningful
653
+ * distinction" quantum {@link reachThreshold}'s own doc invokes). A gap
654
+ * of a full quantum or more means real, separate structure could sit
655
+ * between the two spans, so they count as independent corroboration.
656
+ * Strict `<` (not `<=`): verified against gap 3.1's own "gender equality"
657
+ * root, whose two genuine clusters sit EXACTLY W bytes apart — `<= W`
658
+ * would wrongly merge them into one and break that pinned requirement. */
659
+ function countClusters(spans, W) {
660
+ if (spans.length === 0)
661
+ return 0;
662
+ const sorted = [...spans].sort((a, b) => a[0] - b[0]);
663
+ let clusters = 1;
664
+ let curEnd = sorted[0][1];
665
+ for (let i = 1; i < sorted.length; i++) {
666
+ const [s, e] = sorted[i];
667
+ if (s - curEnd < W) {
668
+ curEnd = Math.max(curEnd, e);
669
+ }
670
+ else {
671
+ clusters++;
672
+ curEnd = e;
673
+ }
674
+ }
675
+ return clusters;
676
+ }
677
+ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg) {
678
+ const { votes, votesIdf, support, regionSupport, regionSpans, steps } = pooled;
455
679
  if (votes.size === 0) {
456
- traceAttention(ctx, regions, regionVoter, [], steps);
680
+ traceAttention(ctx, regions, regionVoter, [], steps, td, cfg);
457
681
  return { roots: [], ranked: [] };
458
682
  }
459
683
  // SCALE-INVARIANT confidence — see Attention.breadth's doc. regions.length
@@ -470,6 +694,7 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N) {
470
694
  start: s.start,
471
695
  end: s.end,
472
696
  breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
697
+ clusters: countClusters(regionSpans.get(anchor) ?? [], ctx.space.maxGroup),
473
698
  };
474
699
  })
475
700
  .sort((a, b) => b.vote - a.vote);
@@ -488,23 +713,92 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N) {
488
713
  const floor = consensusFloor(N);
489
714
  const placed = [];
490
715
  const roots = [];
491
- for (const point of ranked) {
716
+ const recordAnchor = (point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, rejectionReasons) => {
717
+ if (!td)
718
+ return;
719
+ td.anchors.push({
720
+ anchor: point.anchor,
721
+ rank,
722
+ pooledVote: point.vote,
723
+ idfVote: votesIdf.get(point.anchor) ?? 0,
724
+ candidateBreadth: regions.length,
725
+ contributingVotes: regionSpans.get(point.anchor)?.length ?? 0,
726
+ contributingEvidence: regionSupport.get(point.anchor) ?? 0,
727
+ breadth: point.breadth,
728
+ contributingSpans: regionSpans.get(point.anchor) ?? [],
729
+ clusters: point.clusters,
730
+ commit: {
731
+ status,
732
+ dominant,
733
+ passesNaturalBreak,
734
+ passesConsensusFloor,
735
+ pastLeadingSaturation,
736
+ rejectionReasons,
737
+ },
738
+ });
739
+ };
740
+ for (let rank = 0; rank < ranked.length; rank++) {
741
+ const point = ranked[rank];
492
742
  const absorbed = placed.some((p) => overlaps(point, p));
493
- if (!absorbed) {
743
+ // Commit decisions are recorded LIVE, inside this loop, in the exact
744
+ // shape the gates below apply them — never reconstructed afterward from
745
+ // the final `roots` (spec §8's explicit requirement).
746
+ let status;
747
+ let dominant = false;
748
+ let passesNaturalBreak;
749
+ let passesConsensusFloor;
750
+ let pastLeadingSaturation;
751
+ const rejectionReasons = [];
752
+ if (absorbed) {
753
+ status = "overlap";
754
+ }
755
+ else {
494
756
  const pastLeading = !sat.hasLeading ||
495
757
  roots.length === 0 || point.start >= sat.leadingEnd;
758
+ pastLeadingSaturation = pastLeading;
496
759
  const vote = votesIdf.get(point.anchor) ?? 0;
497
- if ((roots.length === 0 || (vote >= rootCut && vote >= floor)) &&
498
- pastLeading) {
760
+ if (roots.length === 0) {
761
+ // The first non-overlapping root is DOMINANT and bypasses the two
762
+ // vote thresholds (it always grounds) — only the leading-saturation
763
+ // gate still applies to it.
764
+ dominant = true;
765
+ if (pastLeading) {
766
+ status = "root";
767
+ }
768
+ else {
769
+ status = "rejected";
770
+ rejectionReasons.push("leading-saturation");
771
+ }
772
+ }
773
+ else {
774
+ passesNaturalBreak = vote >= rootCut;
775
+ passesConsensusFloor = vote >= floor;
776
+ if (passesNaturalBreak && passesConsensusFloor && pastLeading) {
777
+ status = "root";
778
+ }
779
+ else {
780
+ status = "rejected";
781
+ if (!passesNaturalBreak)
782
+ rejectionReasons.push("below-natural-break");
783
+ if (!passesConsensusFloor) {
784
+ rejectionReasons.push("below-consensus-floor");
785
+ }
786
+ if (!pastLeading)
787
+ rejectionReasons.push("leading-saturation");
788
+ }
789
+ }
790
+ if (status === "root") {
499
791
  roots.push(point);
500
792
  }
501
793
  else {
794
+ recordAnchor(point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, rejectionReasons);
502
795
  continue;
503
796
  }
504
797
  }
798
+ recordAnchor(point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, rejectionReasons);
505
799
  placed.push(point);
506
800
  }
507
- traceAttention(ctx, regions, regionVoter, roots, steps);
801
+ traceAttention(ctx, regions, regionVoter, roots, steps, td, cfg ? { ...cfg, naturalBreak: rootCut, consensusFloor: floor } : undefined, ranked);
508
802
  return { roots, ranked };
509
803
  }
510
804
  export function detectSaturated(ctx, regions, saturated) {
@@ -607,63 +901,186 @@ export function naturalBreak(votes) {
607
901
  }
608
902
  return votes[breakAt - 1];
609
903
  }
610
- // ═══════════════════════════════════════════════════════════════════════════
611
- // Cross-region attention — DIRECT region-to-region interaction.
612
- //
613
- // voteRegions climbs each region INDEPENDENTLY; poolVotes then ADDS those
614
- // independent votes. Additive pooling is a soft conjunction, but it can only
615
- // ever surface a context at least one region already votes for. Two regions
616
- // whose individual climbs land on DIFFERENT contexts leave their JOINT context
617
- // the learnt whole that contains BOTH with no vote at all, and no amount
618
- // of pooling can recover it. ("red" climbs to `red square`, "circle" to
619
- // `circle`; nothing votes for `red circle`, the only fact holding both.)
620
- //
621
- // This is the attention counterpart of the bridge, and it ascends by the SAME
622
- // content-addressed junction walk (junction.ts): "which learnt whole contains
623
- // region A then region B?" is a bounded DAG ascent from the two forms'
624
- // canonical identities NOT a resonance guess on a synthesised gist. Folding
625
- // two region vectors cannot even reconstruct the stored joint form: Sema builds
626
- // a multi-word gist from BYTE-chunk folds, so isolated word vectors superpose
627
- // into a different direction and resonate to `red circle` and `red square`
628
- // indistinguishably. The ascent sidesteps this by matching BYTES, not vectors.
629
- //
630
- // A joint container is EXACT evidence (it literally holds both forms), so it
631
- // votes at full strength — the exact-first discipline voteRegions gives a
632
- // content-addressed chunk. Each junction search is a bounded walk with NO
633
- // ANN query, and searches are capped at k. Three further disciplines make
634
- // the composition ORDER-FREE, N-ARY, and CORPUS-INDEPENDENT:
635
- //
636
- // ORDER-FREE a junction is evidence the forms were LEARNT TOGETHER;
637
- // which one the query mentioned first is a fact about the query, not the
638
- // learnt whole. The walk tests both byte orders at no extra walk cost
639
- // (see junctionContainersFrom's `unordered`).
640
- // • N-ARY binding is not intrinsically pairwise. A pair's containers are
641
- // FILTERED by the remaining candidate forms: the container covering the
642
- // MOST of the query's composable forms wins, so three cross-cutting
643
- // attributes (each pair ambiguous) still resolve to their unique triple —
644
- // at the cost of one cached byte read + indexOf per (container, extra),
645
- // never an extra walk.
646
- // • CORPUS-INDEPENDENT — candidates are ANY voted region, not just
647
- // recognised sites. A word never learnt standalone has no site, but its
648
- // stored chunks still vote and their BYTES still compose: the ascent
649
- // matches byte containment, so a fragment pair evidences the same joint
650
- // container the whole word would. Contiguous shards of one word cannot
651
- // pair (the adjacency skip), and a pair covered by a single KNOWN region
652
- // is skipped — that whole form already votes directly, and re-deriving it
653
- // from its own pieces would only double-count.
654
- //
655
- // EXPLAINING AWAY (the aliasing complement of corpus independence): a chunk
656
- // of the query can straddle the byte grid so that it exists verbatim in the
657
- // WRONG deposit (" cir" of "red then circle" is a stored chunk of `blue
658
- // circle`, never of `red circle` — a pure alignment accident) and its
659
- // independent climb then votes for a context the query gives no reason to
660
- // believe. When a junction binds, any individual vote whose bytes the joint
661
- // container LITERALLY CONTAINS yet whose climb disagrees with the junction's
662
- // is superseded: the exact joint evidence explains those bytes, so their
663
- // disagreeing vote is grid aliasing, not signal. Votes whose bytes the
664
- // container does not hold (a genuine second topic) are untouched.
665
- // ═══════════════════════════════════════════════════════════════════════════
666
- async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
904
+ const VARIANT_KIND_ORDER = {
905
+ "exact-exact": -1,
906
+ "left-synonym": 0,
907
+ "right-synonym": 1,
908
+ "double-synonym": 2,
909
+ };
910
+ /** A node's structural gist, read directly from its own stored bytes — the
911
+ * repository's node gist accessor: content is immutable and perception is
912
+ * a pure function of bytes, so re-perceiving a node's full stored bytes
913
+ * reproduces exactly the gist it was interned with. Never concatenates
914
+ * the sibling's bytes with anything else. */
915
+ function storedNodeGist(ctx, id) {
916
+ return gistOf(ctx, read(ctx, id));
917
+ }
918
+ /** A halo sibling's structural part, occupying the ORIGINAL query-region
919
+ * slot length the sibling replaces only the DIRECTION, never the query's
920
+ * own bytes, position, or gap (see §6 of the spec this implements). */
921
+ function siblingPart(ctx, sibling, originalRegion) {
922
+ return {
923
+ v: storedNodeGist(ctx, sibling.id),
924
+ len: originalRegion.end - originalRegion.start,
925
+ };
926
+ }
927
+ /** Build, bound and order every mandatory structural variant (§7-8): the
928
+ * exact/exact composition is always kept; up to `ctx.cfg.haloQueryK`
929
+ * synonym variants (single- and double-synonym combined, one shared
930
+ * budget) are appended, ordered by confidence, then kind, then sibling id. */
931
+ export function buildStructuralVariants(ctx, ra, rb, sides) {
932
+ const exactLeft = { v: ra.v, len: ra.end - ra.start };
933
+ const exactRight = { v: rb.v, len: rb.end - rb.start };
934
+ const synonymVariants = [];
935
+ for (const ls of sides.leftSiblings) {
936
+ synonymVariants.push({
937
+ left: siblingPart(ctx, ls, ra),
938
+ right: exactRight,
939
+ kind: "left-synonym",
940
+ semanticConfidence: ls.score,
941
+ leftSiblingId: ls.id,
942
+ });
943
+ }
944
+ for (const rs of sides.rightSiblings) {
945
+ synonymVariants.push({
946
+ left: exactLeft,
947
+ right: siblingPart(ctx, rs, rb),
948
+ kind: "right-synonym",
949
+ semanticConfidence: rs.score,
950
+ rightSiblingId: rs.id,
951
+ });
952
+ }
953
+ for (const ls of sides.leftSiblings) {
954
+ for (const rs of sides.rightSiblings) {
955
+ synonymVariants.push({
956
+ left: siblingPart(ctx, ls, ra),
957
+ right: siblingPart(ctx, rs, rb),
958
+ kind: "double-synonym",
959
+ semanticConfidence: Math.min(ls.score, rs.score),
960
+ leftSiblingId: ls.id,
961
+ rightSiblingId: rs.id,
962
+ });
963
+ }
964
+ }
965
+ // §8: semantic confidence desc, then kind (left-synonym, right-synonym,
966
+ // double-synonym), then left sibling id asc, then right sibling id asc.
967
+ synonymVariants.sort((a, b) => b.semanticConfidence - a.semanticConfidence ||
968
+ VARIANT_KIND_ORDER[a.kind] - VARIANT_KIND_ORDER[b.kind] ||
969
+ (a.leftSiblingId ?? -1) - (b.leftSiblingId ?? -1) ||
970
+ (a.rightSiblingId ?? -1) - (b.rightSiblingId ?? -1));
971
+ const variants = [
972
+ {
973
+ left: exactLeft,
974
+ right: exactRight,
975
+ kind: "exact-exact",
976
+ semanticConfidence: 1,
977
+ },
978
+ ...synonymVariants.slice(0, ctx.cfg.haloQueryK),
979
+ ];
980
+ return { variants, exactLeft, exactRight };
981
+ }
982
+ /** Deterministic best-of tie-break for two proposals ranked for the SAME
983
+ * candidate id — effectiveScore, then annScore, then semanticConfidence,
984
+ * then variant kind, then sibling ids (§10). */
985
+ function betterProposal(a, b) {
986
+ if (a.effectiveScore !== b.effectiveScore) {
987
+ return a.effectiveScore > b.effectiveScore;
988
+ }
989
+ if (a.annScore !== b.annScore)
990
+ return a.annScore > b.annScore;
991
+ if (a.semanticConfidence !== b.semanticConfidence) {
992
+ return a.semanticConfidence > b.semanticConfidence;
993
+ }
994
+ if (VARIANT_KIND_ORDER[a.variant] !== VARIANT_KIND_ORDER[b.variant]) {
995
+ return VARIANT_KIND_ORDER[a.variant] < VARIANT_KIND_ORDER[b.variant];
996
+ }
997
+ if ((a.leftSiblingId ?? -1) !== (b.leftSiblingId ?? -1)) {
998
+ return (a.leftSiblingId ?? -1) < (b.leftSiblingId ?? -1);
999
+ }
1000
+ return (a.rightSiblingId ?? -1) < (b.rightSiblingId ?? -1);
1001
+ }
1002
+ /** The final approximate tier: compose every retained structural variant,
1003
+ * ANN-query each, merge proposals by candidate id, and validate the winner
1004
+ * through the SAME structural gates every other tier answers to (saturation,
1005
+ * roots, IDF, contrastive margin). Returns null when nothing survives. */
1006
+ export async function structuralResonance(ctx, query, ra, rb, sides, k, N, reachMemo,
1007
+ /** Each side's OWN individual climb roots (from voteRegions), when it cast
1008
+ * one — the self-evidence backstop structural-resonance needs and the
1009
+ * exact tier gets for free from literal byte containment (§11's whole
1010
+ * premise: recover a JOINT context neither side votes for alone). A
1011
+ * candidate whose reach is exactly one side's own conclusion is not new
1012
+ * evidence of a joint whole; it is that side's resonance rediscovering
1013
+ * itself through a synthetic gist still dominated by its own direction. */
1014
+ ownRootsA, ownRootsB) {
1015
+ const { variants } = buildStructuralVariants(ctx, ra, rb, sides);
1016
+ const middleBytes = query.subarray(ra.end, rb.start);
1017
+ const middlePart = middleBytes.length === 0
1018
+ ? null
1019
+ : { v: perceive(ctx, middleBytes).v, len: middleBytes.length };
1020
+ const proposals = new Map();
1021
+ for (const variant of variants) {
1022
+ const parts = [variant.left];
1023
+ if (middlePart)
1024
+ parts.push(middlePart);
1025
+ parts.push(variant.right);
1026
+ const synthetic = composeStructuralGist(ctx.space, parts);
1027
+ const hits = await ctx.store.resonate(synthetic, k);
1028
+ for (const hit of hits) {
1029
+ const candidate = {
1030
+ id: hit.id,
1031
+ annScore: hit.score,
1032
+ semanticConfidence: variant.semanticConfidence,
1033
+ effectiveScore: hit.score * variant.semanticConfidence,
1034
+ variant: variant.kind,
1035
+ leftSiblingId: variant.leftSiblingId,
1036
+ rightSiblingId: variant.rightSiblingId,
1037
+ };
1038
+ const prev = proposals.get(hit.id);
1039
+ if (prev === undefined || betterProposal(candidate, prev)) {
1040
+ proposals.set(hit.id, candidate);
1041
+ }
1042
+ }
1043
+ }
1044
+ if (proposals.size === 0)
1045
+ return null;
1046
+ const sorted = [...proposals.values()].sort((a, b) => b.effectiveScore - a.effectiveScore || a.id - b.id);
1047
+ let selected = null;
1048
+ let selectedReach = null;
1049
+ let selectedIdf = 0;
1050
+ let rival = null;
1051
+ for (const p of sorted) {
1052
+ const reach = edgeAncestors(ctx, p.id, N, reachMemo);
1053
+ if (reach.saturated || reach.roots.length === 0)
1054
+ continue;
1055
+ const idf = Math.log(N / Math.max(1, reach.contextsReached));
1056
+ if (idf <= 0)
1057
+ continue;
1058
+ // Self-evidence backstop (see the param doc above): a candidate that is
1059
+ // exactly one side's own already-voted conclusion carries no JOINT
1060
+ // evidence — skip it as if it never survived.
1061
+ if ((ownRootsA && sameRoots(reach.roots, ownRootsA)) ||
1062
+ (ownRootsB && sameRoots(reach.roots, ownRootsB)))
1063
+ continue;
1064
+ if (selected === null) {
1065
+ selected = p;
1066
+ selectedReach = reach;
1067
+ selectedIdf = idf;
1068
+ }
1069
+ else if (!sameRoots(reach.roots, selectedReach.roots)) {
1070
+ rival = p;
1071
+ break;
1072
+ }
1073
+ }
1074
+ if (selected === null || selectedReach === null)
1075
+ return null;
1076
+ const margin = rival
1077
+ ? selected.effectiveScore - rival.effectiveScore
1078
+ : selected.effectiveScore;
1079
+ if (margin <= estimatorNoise(ctx.store.D))
1080
+ return null;
1081
+ return { proposal: selected, reach: selectedReach, idf: selectedIdf };
1082
+ }
1083
+ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
667
1084
  // Candidate regions: every region that ALREADY CAST ITS OWN VOTE in
668
1085
  // voteRegions — individually idf > 0, genuinely discriminative on its own,
669
1086
  // just not necessarily for the SAME context as its partner. This is the
@@ -713,6 +1130,14 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
713
1130
  regions[x].end <= regions[y].end &&
714
1131
  regions[y].end - regions[y].start > regions[x].end - regions[x].start));
715
1132
  const none = { votes: [], superseded: new Set() };
1133
+ if (td) {
1134
+ td.crossRegionSummary = {
1135
+ eligibleRegions: eligible.length,
1136
+ maximalRegions: cand.length,
1137
+ probeLimit: k,
1138
+ probesAttempted: 0, // updated below as probes accrue
1139
+ };
1140
+ }
716
1141
  if (cand.length < 2)
717
1142
  return none;
718
1143
  cand.sort((x, y) => regions[x].start - regions[y].start || regions[x].end - regions[y].end);
@@ -777,92 +1202,174 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
777
1202
  if (regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end))
778
1203
  continue;
779
1204
  probes++;
1205
+ if (td?.crossRegionSummary) {
1206
+ td.crossRegionSummary.probesAttempted = probes;
1207
+ }
780
1208
  const left = query.subarray(ra.start, ra.end);
781
1209
  const right = query.subarray(rb.start, rb.end);
782
1210
  // Phrase-scale contract, exactly as the bridge: the glue between the two
783
1211
  // forms may be up to W× the content it joins.
784
1212
  const maxInterior = (left.length + right.length) * ctx.space.maxGroup;
785
1213
  const cap = left.length + right.length + maxInterior;
786
- // Tier 1 exact containers (both forms as substrings, either order, by
787
- // DAG ascent). Exact evidence first; only falls through to synonyms
788
- // when the exact ascent finds nothing the SAME graded ladder the
789
- // bridge uses.
1214
+ // The graded ladder (spec §1): exact DAG junction, then single-synonym,
1215
+ // then double-synonym, then only when every DAG tier found nothing —
1216
+ // structural-resonance. `sides` (the two halo sibling lists) is loaded
1217
+ // ONCE and reused by junctionSynonyms AND structural-resonance, so no
1218
+ // ladder rung repeats a halo ANN query an earlier rung already paid for.
1219
+ const sides = await loadJunctionSynonymSides(ctx, left, right);
1220
+ let tier = "exact";
790
1221
  let containers = junctionContainersFrom(ctx, left, right, cap, seedsOf(cand[a]), seedsOf(cand[b]), undefined, true);
791
1222
  if (containers.length === 0) {
792
- // Tier 2.5 — synonym containers (halo sibling of one side + the other).
793
- containers = await junctionSynonyms(ctx, left, right, maxInterior, true);
1223
+ // Tiers 2-4 — synonym containers (junctionSynonyms itself runs
1224
+ // single-synonym first, falling to double-synonym only when
1225
+ // single-synonym found nothing — see junction.ts).
1226
+ const syn = await junctionSynonyms(ctx, left, right, maxInterior, true, sides);
1227
+ if (syn.length > 0) {
1228
+ containers = syn;
1229
+ tier = syn[0].tier;
1230
+ }
1231
+ }
1232
+ // Tier 5 — structural-resonance ANN, the FINAL approximate proposal
1233
+ // path. Only reached when every DAG tier found NOTHING, and only when
1234
+ // there is no already-corroborated region between the endpoints (a
1235
+ // between-region with its own vote is evidence the gap already means
1236
+ // something specific — an ANN guess must not override it).
1237
+ let structuralPick = null;
1238
+ if (containers.length === 0) {
1239
+ // Structural-resonance composes each side's OWN gist directly (no
1240
+ // byte-containment truth backs it, unlike the DAG tiers) — so, unlike
1241
+ // the DAG ladder (which tolerates one approximate side because byte
1242
+ // containment cannot lie), the ANN tier requires BOTH sides to be
1243
+ // KNOWN (content-addressed, exact identities): an approximate chunk
1244
+ // fragment's own resonance is noise at any tier, and composing noise
1245
+ // into a synthetic gist only manufactures a plausible-looking but
1246
+ // spurious ANN neighbour, not evidence of a genuine joint whole.
1247
+ // PHRASE-SCALE CONTRACT — the same one the DAG tiers hold their glue
1248
+ // to (see maxInterior above): a junction, exact or approximate, is a
1249
+ // whole the two forms nearly exhaust, not two arbitrary landmarks
1250
+ // anywhere in a long, multi-topic query. Without this, structural-
1251
+ // resonance would pair opposite ends of an unrelated scaffolding-
1252
+ // dominated query and manufacture a plausible-looking ANN neighbour
1253
+ // for a "gap" that never was a phrase.
1254
+ // BOTH sides must be independently DISCRIMINATIVE (individually
1255
+ // voted — `strong`, not merely a content-addressed `known` chunk):
1256
+ // a shared, non-discriminative scaffolding run (a repeated system
1257
+ // preamble) can be `known` without ever being distinctive evidence
1258
+ // of anything, and composing its own gist into a synthetic query
1259
+ // manufactures a plausible-looking but spurious ANN neighbour. The
1260
+ // DAG tiers can tolerate one merely-`known` side because byte
1261
+ // containment cannot lie; structural-resonance has no such
1262
+ // backstop, so both sides earn their place here the same way an
1263
+ // ordinary approximate region earns its individual vote.
1264
+ const gap = rb.start - ra.end;
1265
+ if (between.length === 0 &&
1266
+ strong.has(cand[a]) && strong.has(cand[b]) &&
1267
+ ra.known && rb.known &&
1268
+ gap <= maxInterior) {
1269
+ const ownRootsA = rvs.votes.find((v) => v.start === ra.start && v.end === ra.end)?.roots;
1270
+ const ownRootsB = rvs.votes.find((v) => v.start === rb.start && v.end === rb.end)?.roots;
1271
+ structuralPick = await structuralResonance(ctx, query, ra, rb, sides, k, N, reachMemo, ownRootsA, ownRootsB);
1272
+ }
1273
+ if (structuralPick === null)
1274
+ continue;
1275
+ tier = "structural-resonance";
794
1276
  }
795
- if (containers.length === 0)
796
- continue;
797
- // N-ARY selection: the container covering the MOST remaining candidate
798
- // forms wins (then tightest interior, then lowest id). Reads are
799
- // cache hits — every container's bytes were already read by the walk.
800
- //
801
- // SELF-EVIDENCE GUARD: a junction is BINDING evidence only when the
802
- // container joins forms the query mentions APART. When the container's
803
- // own joined occurrence (left..right including its interior) is a
804
- // literal substring of the query, the query already spells that phrase
805
- // out contiguously — perception already voted with it, and grid shards
806
- // of one phrase pairing "around" a gap chunk would merely rediscover
807
- // the phrase they are shards of, then explain away its rivals.
808
1277
  let best = null;
809
1278
  let bestExtras = [];
810
1279
  let bestCov = -1;
811
- for (const c of containers) {
812
- const bytes = cachedRead(ctx, cache, c.id, cap);
813
- const li = indexOf(bytes, left, 0);
814
- const ri = indexOf(bytes, right, 0);
815
- if (li >= 0 && ri >= 0) {
816
- const joined = bytes.subarray(Math.min(li, ri), Math.max(li + left.length, ri + right.length));
817
- if (indexOf(query, joined, 0) >= 0)
818
- continue; // query says it itself
819
- }
820
- // CONTRADICTION GUARD: a between-region already carrying its own
821
- // vote must actually recur in this container's bytes — otherwise
822
- // the container is a different learnt whole that happens to share
823
- // ra/rb, and letting it stand in for the gap would silently
824
- // override evidence the query itself already resolved there.
825
- if (between.some((bi) => indexOf(bytes, query.subarray(regions[bi].start, regions[bi].end), 0) < 0)) {
826
- continue;
827
- }
828
- let cov = left.length + right.length;
829
- const extras = [];
830
- for (const ei of cand) {
831
- if (ei === cand[a] || ei === cand[b] || consumed.has(ei))
832
- continue;
833
- const e = regions[ei];
834
- if (overlapsSpan(e, ra) || overlapsSpan(e, rb))
1280
+ let reach;
1281
+ let idf;
1282
+ let confidence;
1283
+ if (structuralPick !== null) {
1284
+ // A resonance proposal is NOT a Junction — there is no container to
1285
+ // read bytes from, so the self-evidence/contradiction/N-ary
1286
+ // machinery below (byte-verified against a real container) does not
1287
+ // apply; per spec §13, no N-ary extra-region coverage for resonance
1288
+ // proposals.
1289
+ best = { id: structuralPick.proposal.id, interior: new Uint8Array(0) };
1290
+ bestExtras = [];
1291
+ bestCov = rb.end - ra.start;
1292
+ reach = structuralPick.reach;
1293
+ idf = structuralPick.idf;
1294
+ confidence = structuralPick.proposal.effectiveScore;
1295
+ }
1296
+ else {
1297
+ // N-ARY selection: the container covering the MOST remaining candidate
1298
+ // forms wins (then tightest interior, then lowest id). Reads are
1299
+ // cache hits every container's bytes were already read by the walk.
1300
+ //
1301
+ // SELF-EVIDENCE GUARD: a junction is BINDING evidence only when the
1302
+ // container joins forms the query mentions APART. When the container's
1303
+ // own joined occurrence (left..right including its interior) is a
1304
+ // literal substring of the query, the query already spells that phrase
1305
+ // out contiguously — perception already voted with it, and grid shards
1306
+ // of one phrase pairing "around" a gap chunk would merely rediscover
1307
+ // the phrase they are shards of, then explain away its rivals.
1308
+ for (const c of containers) {
1309
+ const bytes = cachedRead(ctx, cache, c.id, cap);
1310
+ const li = indexOf(bytes, left, 0);
1311
+ const ri = indexOf(bytes, right, 0);
1312
+ if (li >= 0 && ri >= 0) {
1313
+ const joined = bytes.subarray(Math.min(li, ri), Math.max(li + left.length, ri + right.length));
1314
+ if (indexOf(query, joined, 0) >= 0)
1315
+ continue; // query says it itself
1316
+ }
1317
+ // CONTRADICTION GUARD: a between-region already carrying its own
1318
+ // vote must actually recur in this container's bytes — otherwise
1319
+ // the container is a different learnt whole that happens to share
1320
+ // ra/rb, and letting it stand in for the gap would silently
1321
+ // override evidence the query itself already resolved there.
1322
+ if (between.some((bi) => indexOf(bytes, query.subarray(regions[bi].start, regions[bi].end), 0) < 0)) {
835
1323
  continue;
836
- const eb = query.subarray(e.start, e.end);
837
- if (indexOf(bytes, eb, 0) >= 0) {
838
- extras.push(ei);
839
- cov += eb.length;
1324
+ }
1325
+ let cov = left.length + right.length;
1326
+ const extras = [];
1327
+ for (const ei of cand) {
1328
+ if (ei === cand[a] || ei === cand[b] || consumed.has(ei))
1329
+ continue;
1330
+ const e = regions[ei];
1331
+ if (overlapsSpan(e, ra) || overlapsSpan(e, rb))
1332
+ continue;
1333
+ const eb = query.subarray(e.start, e.end);
1334
+ if (indexOf(bytes, eb, 0) >= 0) {
1335
+ extras.push(ei);
1336
+ cov += eb.length;
1337
+ }
1338
+ }
1339
+ if (cov > bestCov ||
1340
+ (cov === bestCov && best !== null &&
1341
+ (c.interior.length < best.interior.length ||
1342
+ (c.interior.length === best.interior.length && c.id < best.id)))) {
1343
+ best = c;
1344
+ bestExtras = extras;
1345
+ bestCov = cov;
840
1346
  }
841
1347
  }
842
- if (cov > bestCov ||
843
- (cov === bestCov && best !== null &&
844
- (c.interior.length < best.interior.length ||
845
- (c.interior.length === best.interior.length && c.id < best.id)))) {
846
- best = c;
847
- bestExtras = extras;
848
- bestCov = cov;
849
- }
1348
+ if (best === null)
1349
+ continue; // every container was self-evidence
1350
+ const r = edgeAncestors(ctx, best.id, N, reachMemo);
1351
+ if (r.saturated || r.roots.length === 0)
1352
+ continue;
1353
+ const df = Math.log(N / Math.max(1, r.contextsReached));
1354
+ if (df <= 0)
1355
+ continue;
1356
+ reach = r;
1357
+ idf = df;
1358
+ // Confidence used by voting (spec §13): exact junction = 1;
1359
+ // single/double-synonym = the sibling(s)' score(s), carried on the
1360
+ // SynonymJunction the ladder selected.
1361
+ confidence = "confidence" in best ? best.confidence : 1;
850
1362
  }
851
- if (best === null)
852
- continue; // every container was self-evidence
853
- const reach = edgeAncestors(ctx, best.id, N, reachMemo);
854
- if (reach.saturated || reach.roots.length === 0)
855
- continue;
856
- const idf = Math.log(N / Math.max(1, reach.contextsReached));
857
- if (idf <= 0)
858
- continue;
859
- // EXACT joint evidence (score = 1): the container literally contains
860
- // every composed form. Mutual-explanation weight over their COMBINED
861
- // byte length — the same magnitude reading voteRegions uses, here with
862
- // the estimator collapsed to certainty, so mutual = min(ratio, 1/ratio).
1363
+ // MUTUAL-EXPLANATION WEIGHT — the same formula for every tier, with
1364
+ // `confidence` collapsed to certainty (1) for exact evidence: under
1365
+ // that collapse this is byte-for-byte the old exact-only formula
1366
+ // (min(1,ratio)·min(1,1/ratio)). For structural-resonance,
1367
+ // `confidence` is already annScore·semanticConfidence — never
1368
+ // multiplied a second time.
863
1369
  const lenR = Math.max(1, bestCov);
864
1370
  const ratio = Math.sqrt(Math.max(1, ctx.store.contentLen(best.id, lenR * ctx.store.D)) / lenR);
865
- const mutual = Math.min(1, ratio) * Math.min(1, 1 / ratio);
1371
+ const mutual = Math.min(1, confidence * ratio) *
1372
+ Math.min(1, confidence / ratio);
866
1373
  const w = (mutual * idf) / reach.roots.length;
867
1374
  let spanStart = ra.start;
868
1375
  let spanEnd = rb.end;
@@ -884,16 +1391,33 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
884
1391
  // for, not evidence lost — `absorbed` (RegionVote's breadth-accounting
885
1392
  // field) must credit the junction with all of it, not just the ONE
886
1393
  // pooled axiom it collapses to.
887
- const containerBytes = cachedRead(ctx, cache, best.id, cap);
888
- const jointRoots = new Set(reach.roots);
1394
+ // Only EXACT DAG evidence may explain away ordinary votes (spec §15).
1395
+ // Single-synonym, double-synonym, and structural-resonance may ADD
1396
+ // supporting evidence but never remove it: their evidence is itself
1397
+ // approximate (a sibling substitution, or an ANN guess), so treating
1398
+ // their byte-containment the way exact containment is treated would
1399
+ // let an approximation override a genuine, independently-voted region.
889
1400
  let explainedAway = 0;
890
- for (const rv of rvs.votes) {
891
- if (rv.roots.some((r) => jointRoots.has(r)))
892
- continue;
893
- const bytes = query.subarray(rv.start, rv.end);
894
- if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
895
- superseded.add(rv);
896
- explainedAway++;
1401
+ // Exact set of ORIGINAL region indices this junction explained away —
1402
+ // recorded live as `superseded.add` fires (spec §3's explicit rule:
1403
+ // never inferred from `absorbed` afterward).
1404
+ const explainedAwayIndices = [];
1405
+ if (tier === "exact") {
1406
+ const containerBytes = cachedRead(ctx, cache, best.id, cap);
1407
+ const jointRoots = new Set(reach.roots);
1408
+ for (const rv of rvs.votes) {
1409
+ if (rv.roots.some((r) => jointRoots.has(r)))
1410
+ continue;
1411
+ const bytes = query.subarray(rv.start, rv.end);
1412
+ if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
1413
+ superseded.add(rv);
1414
+ explainedAway++;
1415
+ if (td) {
1416
+ const idx = regions.findIndex((r) => r.start === rv.start && r.end === rv.end);
1417
+ if (idx >= 0)
1418
+ explainedAwayIndices.push(idx);
1419
+ }
1420
+ }
897
1421
  }
898
1422
  }
899
1423
  out.push({
@@ -905,28 +1429,61 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
905
1429
  wFocus: w,
906
1430
  absorbed: 1 + explainedAway,
907
1431
  });
1432
+ if (td) {
1433
+ td.crossRegionJunctionVotes.push({
1434
+ container: best.id,
1435
+ span: [spanStart, spanEnd],
1436
+ roots: [...reach.roots],
1437
+ sourceRegionIndices: [cand[a], cand[b], ...bestExtras],
1438
+ explainedAwayRegionIndices: explainedAwayIndices,
1439
+ absorbed: 1 + explainedAway,
1440
+ tier,
1441
+ });
1442
+ }
908
1443
  const label = [cand[a], cand[b], ...bestExtras]
909
1444
  .sort((x, y) => regions[x].start - regions[y].start)
910
1445
  .map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
911
1446
  .join(" ▸ ");
1447
+ const tierNote = tier === "exact"
1448
+ ? `junction node ${best.id}` +
1449
+ (best.interior.length === 0
1450
+ ? " (adjacent)"
1451
+ : ` (interior "${dec(best.interior)}")`) +
1452
+ ", by content-addressed ascent"
1453
+ : tier === "structural-resonance"
1454
+ ? `structurally-composed ANN proposal, node ${best.id} — the query ` +
1455
+ `structurally composed the endpoint regions, the real middle-` +
1456
+ `query structure, and the selected halo-sibling endpoint ` +
1457
+ `direction(s) (variant ${structuralPick.proposal.variant}, ` +
1458
+ `annScore ${structuralPick.proposal.annScore.toFixed(3)} × ` +
1459
+ `semanticConfidence ${structuralPick.proposal.semanticConfidence.toFixed(3)} = effectiveScore ${structuralPick.proposal.effectiveScore.toFixed(3)}); it did not concatenate endpoint bytes or rewrite the query`
1460
+ : `${tier} junction node ${best.id}` +
1461
+ (best.interior.length === 0
1462
+ ? " (adjacent)"
1463
+ : ` (interior "${dec(best.interior)}")`) +
1464
+ `, by halo-sibling DAG ascent (confidence ${confidence.toFixed(3)})`;
912
1465
  ctx.trace?.step("crossRegion", [{ text: label, role: "pair" }], reach.roots.map((r) => ({
913
1466
  text: dec(read(ctx, r)).slice(0, 60),
914
1467
  node: r,
915
1468
  role: "joint-context",
916
- })), `${label} → junction node ${best.id}` +
917
- (best.interior.length === 0
918
- ? " (adjacent)"
919
- : ` (interior "${dec(best.interior)}")`) +
920
- ` → ${reach.roots.length} context(s), by content-addressed ascent` +
1469
+ })), `${label} → ${tierNote} ${reach.roots.length} context(s)` +
921
1470
  (superseded.size > 0
922
1471
  ? `; ${superseded.size} aliasing vote(s) explained away`
923
1472
  : ""));
924
1473
  break; // ra is consumed — move to the next unconsumed candidate
925
1474
  }
926
1475
  }
1476
+ if (td)
1477
+ td.supersededOrdinaryVotes = superseded.size;
927
1478
  return { votes: out, superseded };
928
1479
  }
929
- export function traceAttention(ctx, regions, regionVoter, roots, steps = []) {
1480
+ /** Emit the "climbConsensus" step the human-readable note this always
1481
+ * produced, now paired (when `ctx.trace` and `cfg` are both present) with
1482
+ * the structured {@link ClimbConsensusData} payload on the SAME step's
1483
+ * `data` field. Every exit of {@link computeAttention} funnels through
1484
+ * here, so instrumentation and the existing rationale text can never drift
1485
+ * apart — see the instrumentation spec's §9 "every exit path". */
1486
+ export function traceAttention(ctx, regions, regionVoter, roots, steps = [], td, cfg, ranked = roots) {
930
1487
  if (!ctx.trace)
931
1488
  return;
932
1489
  const voters = [];
@@ -943,9 +1500,50 @@ export function traceAttention(ctx, regions, regionVoter, roots, steps = []) {
943
1500
  // shape {@link GraphSearch}'s own cover steps take (see traceDerivation).
944
1501
  if (steps.length > 0)
945
1502
  traceDerivation(ctx, steps);
1503
+ const data = (td && cfg)
1504
+ ? {
1505
+ version: 1,
1506
+ cache: { hit: false, detailAvailable: true },
1507
+ config: {
1508
+ annK: cfg.k,
1509
+ crossRegionProbeLimit: cfg.k,
1510
+ mode: cfg.mode,
1511
+ ...(cfg.N !== undefined ? { corpusN: cfg.N } : {}),
1512
+ dimension: ctx.store.D,
1513
+ ...(cfg.N !== undefined ? { hubBound: hubBound(ctx) } : {}),
1514
+ estimatorNoise: estimatorNoise(ctx.store.D),
1515
+ ...(cfg.naturalBreak !== undefined
1516
+ ? { naturalBreak: cfg.naturalBreak }
1517
+ : {}),
1518
+ ...(cfg.consensusFloor !== undefined
1519
+ ? { consensusFloor: cfg.consensusFloor }
1520
+ : {}),
1521
+ },
1522
+ candidates: {
1523
+ perceived: cfg.perceivedCount,
1524
+ recognised: cfg.totalRegions - cfg.perceivedCount,
1525
+ total: cfg.totalRegions,
1526
+ },
1527
+ ...(td.regions.length > 0 ? { regions: td.regions } : {}),
1528
+ ...(cfg.reachMemo ? { reaches: serialiseReaches(cfg.reachMemo) } : {}),
1529
+ ...(td.crossRegionSummary
1530
+ ? {
1531
+ crossRegion: {
1532
+ ...td.crossRegionSummary,
1533
+ junctionVotes: td.crossRegionJunctionVotes,
1534
+ supersededOrdinaryVotes: td.supersededOrdinaryVotes,
1535
+ },
1536
+ }
1537
+ : {}),
1538
+ ...(td.saturation ? { saturation: td.saturation } : {}),
1539
+ ...(td.pooling ? { pooling: td.pooling } : {}),
1540
+ ...(td.anchors.length > 0 ? { anchors: td.anchors } : {}),
1541
+ result: { roots: [...roots], ranked: [...ranked] },
1542
+ }
1543
+ : undefined;
946
1544
  t.done(roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)), roots.length === 0
947
1545
  ? `${regions.length} sub-regions climbed the DAG, but none agreed on a context`
948
1546
  : roots.length === 1
949
1547
  ? `${voters.length} of ${regions.length} sub-regions voted; IDF-weighted consensus picked one context (vote ${roots[0].vote.toFixed(2)})`
950
- : `${voters.length} of ${regions.length} sub-regions voted; consensus ordered ${roots.length} INDEPENDENT points of attention (votes ${roots.map((r) => r.vote.toFixed(2)).join(", ")})`);
1548
+ : `${voters.length} of ${regions.length} sub-regions voted; consensus ordered ${roots.length} INDEPENDENT points of attention (votes ${roots.map((r) => r.vote.toFixed(2)).join(", ")})`, data);
951
1549
  }