@hviana/sema 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/AGENTS.md +71 -5
  2. package/dist/src/derive/src/deduction.d.ts +12 -1
  3. package/dist/src/derive/src/deduction.js +5 -1
  4. package/dist/src/derive/src/index.d.ts +1 -0
  5. package/dist/src/geometry.d.ts +3 -30
  6. package/dist/src/geometry.js +330 -82
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/src/meter.d.ts +171 -0
  10. package/dist/src/meter.js +269 -0
  11. package/dist/src/mind/attention.d.ts +5 -4
  12. package/dist/src/mind/attention.js +254 -23
  13. package/dist/src/mind/bridge.d.ts +10 -1
  14. package/dist/src/mind/bridge.js +179 -10
  15. package/dist/src/mind/canonical.d.ts +6 -1
  16. package/dist/src/mind/canonical.js +6 -1
  17. package/dist/src/mind/graph-search.d.ts +9 -0
  18. package/dist/src/mind/graph-search.js +59 -19
  19. package/dist/src/mind/index.d.ts +2 -0
  20. package/dist/src/mind/junction.d.ts +10 -0
  21. package/dist/src/mind/junction.js +14 -0
  22. package/dist/src/mind/learning.d.ts +32 -4
  23. package/dist/src/mind/learning.js +26 -4
  24. package/dist/src/mind/match.d.ts +40 -0
  25. package/dist/src/mind/match.js +125 -1
  26. package/dist/src/mind/mechanisms/cast.js +63 -6
  27. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  28. package/dist/src/mind/mechanisms/extraction.js +1 -88
  29. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  30. package/dist/src/mind/mechanisms/recall.js +77 -14
  31. package/dist/src/mind/mind.d.ts +59 -5
  32. package/dist/src/mind/mind.js +115 -93
  33. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  34. package/dist/src/mind/pipeline-mechanism.js +179 -10
  35. package/dist/src/mind/pipeline.d.ts +29 -0
  36. package/dist/src/mind/pipeline.js +79 -21
  37. package/dist/src/mind/primitives.d.ts +11 -15
  38. package/dist/src/mind/primitives.js +47 -28
  39. package/dist/src/mind/reasoning.d.ts +7 -1
  40. package/dist/src/mind/reasoning.js +40 -8
  41. package/dist/src/mind/recognition.js +93 -20
  42. package/dist/src/mind/traverse.d.ts +11 -0
  43. package/dist/src/mind/traverse.js +88 -7
  44. package/dist/src/mind/types.d.ts +39 -5
  45. package/dist/src/store.d.ts +15 -0
  46. package/dist/src/store.js +91 -6
  47. package/package.json +1 -1
  48. package/src/derive/src/deduction.ts +15 -0
  49. package/src/derive/src/index.ts +1 -0
  50. package/src/geometry.ts +350 -122
  51. package/src/index.ts +1 -0
  52. package/src/meter.ts +333 -0
  53. package/src/mind/attention.ts +276 -31
  54. package/src/mind/bridge.ts +187 -10
  55. package/src/mind/canonical.ts +6 -1
  56. package/src/mind/graph-search.ts +60 -21
  57. package/src/mind/index.ts +6 -0
  58. package/src/mind/junction.ts +12 -0
  59. package/src/mind/learning.ts +46 -5
  60. package/src/mind/match.ts +146 -1
  61. package/src/mind/mechanisms/cast.ts +62 -6
  62. package/src/mind/mechanisms/extraction.ts +2 -103
  63. package/src/mind/mechanisms/recall.ts +84 -17
  64. package/src/mind/mind.ts +144 -99
  65. package/src/mind/pipeline-mechanism.ts +203 -13
  66. package/src/mind/pipeline.ts +144 -36
  67. package/src/mind/primitives.ts +49 -33
  68. package/src/mind/reasoning.ts +39 -7
  69. package/src/mind/recognition.ts +89 -19
  70. package/src/mind/traverse.ts +89 -8
  71. package/src/mind/types.ts +39 -5
  72. package/src/store.ts +75 -6
  73. package/test/14-scaling.test.mjs +17 -7
  74. package/test/31-audit.test.mjs +4 -1
  75. package/test/33-multi-candidate.test.mjs +13 -1
  76. package/test/36-already-answered-fusion.test.mjs +10 -3
  77. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  78. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  79. package/test/54-evidence-k-instrumentation.test.mjs +175 -0
  80. package/test/55-cost-meter.test.mjs +284 -0
  81. package/test/56-bridge-identity-admission.test.mjs +209 -0
  82. package/test/57-fusion-order.test.mjs +104 -0
  83. package/test/58-subquantum-sites.test.mjs +112 -0
  84. package/test/59-fold-invariance.test.mjs +226 -0
@@ -12,7 +12,7 @@ import { composeStructuralGist, consensusFloor, dominates, estimatorNoise, } fro
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, hubBound } from "./traverse.js";
15
+ import { corpusN, edgeAncestors, hubBound, sharedReachMemo, } from "./traverse.js";
16
16
  import { cachedRead, junctionContainersFrom, junctionSeeds, junctionSynonyms, loadJunctionSynonymSides, walkCache, } from "./junction.js";
17
17
  import { indexOf } from "../bytes.js";
18
18
  import { rItem, rNode, traceDerivation } from "./trace.js";
@@ -38,6 +38,9 @@ function serialiseReaches(reachMemo) {
38
38
  contextsReached: r.contextsReached,
39
39
  saturated: r.saturated,
40
40
  ...(r.saturation ? { saturation: r.saturation } : {}),
41
+ ...(r.visited !== undefined
42
+ ? { visited: r.visited, maxDepth: r.maxDepth }
43
+ : {}),
41
44
  });
42
45
  }
43
46
  return out;
@@ -73,6 +76,8 @@ export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
73
76
  }
74
77
  const hit = byRead.get(modeKey);
75
78
  if (hit !== undefined) {
79
+ if (ctx.meter)
80
+ ctx.meter.climbHits++;
76
81
  // Cache-hit exit (spec §9): the abbreviated payload shape — only what
77
82
  // is actually stored in the cached AttentionRead is reported. No
78
83
  // candidate, reach, saturation, pooling or anchor detail is fabricated
@@ -98,6 +103,8 @@ export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
98
103
  }
99
104
  // ── Pipeline ──────────────────────────────────────────────────────────────
100
105
  export async function computeAttention(ctx, query, k, mode) {
106
+ if (ctx.meter)
107
+ ctx.meter.climbs++;
101
108
  const regions = collectRegions(ctx, query);
102
109
  const perceivedCount = regions.length;
103
110
  // Recognised sites carry structural evidence that perceived sub-regions
@@ -114,6 +121,22 @@ export async function computeAttention(ctx, query, k, mode) {
114
121
  v: gistOf(ctx, query.subarray(s.start, s.end)),
115
122
  start: s.start,
116
123
  end: s.end,
124
+ // NOT a chunk — a precondition, not a judgement about the evidence.
125
+ // `chunk` admits a region into the saturated-INTERVAL builder (see
126
+ // crossRegionVotes), which walks regions as a SEQUENCE and merges
127
+ // neighbouring saturated ones into runs. Its own contract requires the
128
+ // regions it reads to be DISJOINT and in byte order — true of
129
+ // leaf-parents, false of sites, which overlap each other and the chunks
130
+ // ("red", "circle" and "red circle" are all present at once).
131
+ //
132
+ // Admitting them was measured both ways and is unprincipled in each
133
+ // direction: a saturated site EXTENDS a run and masks votes that should
134
+ // have won (test/37 lost all three — roots became "red " and "hat"
135
+ // instead of "red circle" and "2"), while a non-saturated one BREAKS a
136
+ // run and unmasks votes that should have been dropped, which is the only
137
+ // reason it appeared to fix test/34. Either way the outcome turns on
138
+ // where an overlapping span happens to fall in the array — the same
139
+ // positional accident this work exists to remove.
117
140
  chunk: false,
118
141
  known: true, // a recognised site IS a stored form
119
142
  });
@@ -137,15 +160,25 @@ export async function computeAttention(ctx, query, k, mode) {
137
160
  const N = corpusN(ctx);
138
161
  // One climb per distinct anchor for the WHOLE query: regions sharing a
139
162
  // chunk, and canonicalChunkId's prefix probes, all hit this memo instead of
140
- // re-reading the anchor's full edge fan-out from the store.
141
- const reachMemo = new Map();
142
- const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo, td);
163
+ // re-reading the anchor's full edge fan-out from the store. The memo is
164
+ // the SHARED one (traverse.ts) — response-scoped for respond(),
165
+ // conversation-scoped across turns, and the same map confluence prices
166
+ // commonality against; it used to be a private per-climb Map, so a
167
+ // conversation re-climbed its own repeated regions from cold on every
168
+ // turn. A traced response still gets a fresh one — see sharedReachMemo.
169
+ const reachMemo = sharedReachMemo(ctx);
170
+ const rvs = ctx.meter
171
+ ? await ctx.meter.time("climb.voteRegions", () => voteRegions(ctx, query, regions, k, mode, N, reachMemo, td))
172
+ : await voteRegions(ctx, query, regions, k, mode, N, reachMemo, td);
143
173
  // ── Cross-region: DIRECT region-to-region interaction ─────────────────
144
174
  // Two regions whose individual climbs land on DIFFERENT contexts leave
145
175
  // their JOINT context — the learnt whole that contains BOTH — with no
146
176
  // vote. crossRegionVotes recovers it by the bridge's content-addressed
147
177
  // junction ascent (see the note above the function).
148
- const cross = await crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td);
178
+ const crossArgs = () => crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td);
179
+ const cross = ctx.meter
180
+ ? await ctx.meter.time("climb.crossRegion", crossArgs)
181
+ : await crossArgs();
149
182
  // A vote SUPERSEDED by exact joint evidence (its bytes literally live
150
183
  // inside the joint container, yet it climbed elsewhere — grid aliasing)
151
184
  // is dropped, not down-weighted: the joint container explains it away.
@@ -201,7 +234,17 @@ export function collectRegions(ctx, query) {
201
234
  foldTree(ctx, perceive(ctx, query), 0, (n, start, end, node) => {
202
235
  if (n.kids === null)
203
236
  return;
204
- if (!dominates(end - start, query.length) || regions.length === 0) {
237
+ // The dominance filter is about WRAPPERS, not about size. A chunk is the
238
+ // smallest grouped unit — it wraps no other region — so it can never be the
239
+ // "broad, non-discriminative wrapper" this rule exists to exclude, however
240
+ // much of a short query it happens to cover. Testing it by span alone was
241
+ // safe only while chunks were exactly W bytes: content-defined segments run
242
+ // up to the keyring's seat count, so on a 15-byte query the 8-byte segment
243
+ // "is frigi" counted as dominant and was discarded, leaving CAST one point
244
+ // of attention where it needs two (test/29 D1/D2). Composites are still
245
+ // filtered exactly as before.
246
+ if (isChunk(n) || !dominates(end - start, query.length) ||
247
+ regions.length === 0) {
205
248
  regions.push({
206
249
  v: n.v,
207
250
  start,
@@ -210,15 +253,40 @@ export function collectRegions(ctx, query) {
210
253
  known: node !== null,
211
254
  });
212
255
  }
256
+ // MEASURED AND REFUTED — subdividing a long segment into W-scale tiles.
257
+ // A content segment runs from W−1 up to the keyring's seat count (2W) and
258
+ // folds FLAT, so its only sub-units are single bytes; the grid's regions
259
+ // were always exactly W. Offering each segment's W-byte tiles as extra
260
+ // regions (gists only, no stored nodes, anchored on the segment's own
261
+ // content-defined start so invariance is kept) does restore that finer
262
+ // grain: on `How is ice like steel?` the climb went from ONE ranked anchor
263
+ // to three, and test/33's own CAST-candidate spread was recovered.
264
+ //
265
+ // It is still wrong, and net worse (measured: test/29 went 9/2 to 7/4).
266
+ // canonicalWindows governs EXACT identity lookup, which recognition
267
+ // already probes at every offset — it says nothing about the grain of an
268
+ // approximate gist, so "the write side's unit scale" was two machineries
269
+ // conflated. What the tiles actually do is reintroduce a fixed stride
270
+ // inside the segment, and the extra votes reorder the climb: on
271
+ // `How is Shakespeare like Leonardo da Vinci?` the short name deposits
272
+ // outranked the exemplar sentences, claimed their aligned runs first, and
273
+ // left the sentences CAST needs with no free run at all (C2, C3). A
274
+ // region must come from the fold, not from a stride over it.
213
275
  });
214
276
  return regions;
215
277
  }
216
278
  export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td) {
279
+ if (ctx.meter)
280
+ ctx.meter.climbRegions += regions.length;
217
281
  const regionSaturated = new Array(regions.length).fill(false);
218
282
  const regionVotes = [];
219
283
  const regionVoter = ctx.trace ? regions.map(() => null) : [];
284
+ const W = ctx.space.maxGroup;
220
285
  for (let ri = 0; ri < regions.length; ri++) {
221
- const { v, start, end, chunk, known } = regions[ri];
286
+ // `v`/`start`/`end` are rebindable: a long approximate segment may vote
287
+ // with the sub-span that actually carries its evidence — see below.
288
+ let { v, start, end } = regions[ri];
289
+ const { chunk, known } = regions[ri];
222
290
  // Trace-only bookkeeping for this region — allocated only under `td`
223
291
  // (i.e. only when ctx.trace is set); see ConsensusRegionTrace/
224
292
  // RegionOutcome (spec §4). `examinedIds` tracks distinct ANN hits
@@ -259,10 +327,10 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
259
327
  // the resonate() call for most exact regions — the single largest
260
328
  // remaining inference sink — with the anchor choice unchanged (the
261
329
  // canonical branch already ignored hits[0]).
262
- const canonicalId = chunk
330
+ let canonicalId = chunk
263
331
  ? canonicalChunkId(ctx, query.subarray(start, end), N, reachMemo)
264
332
  : null;
265
- const canonicalUsable = canonicalId !== null &&
333
+ let canonicalUsable = canonicalId !== null &&
266
334
  (ctx.store.hasParents(canonicalId) ||
267
335
  ctx.store.hasContainers(canonicalId));
268
336
  let hits = null;
@@ -273,6 +341,74 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
273
341
  }
274
342
  return hits;
275
343
  };
344
+ // A DILUTED SEGMENT VOTES WITH THE SPAN THAT CARRIES ITS EVIDENCE.
345
+ //
346
+ // A content segment runs up to the keyring's seat count and folds FLAT, so
347
+ // its gist superposes every one of its bytes: an entity inside a longer
348
+ // segment is averaged together with whatever scaffolding shares the
349
+ // segment, and the resonance reads the average. Measured on
350
+ // `How is ice like steel?` against a store holding `Steel is hard`: the
351
+ // segment `ike stee` resonates to `Ice is c` at 0.297 — the WRONG deposit —
352
+ // with `Steel ` fourth at 0.123, while the sub-span `stee` resonates to
353
+ // `Steel ` at 0.627. The evidence is there; the whole-segment read cannot
354
+ // see it, and `Steel is hard` received no vote at all (test/29 C1).
355
+ //
356
+ // Entered only after the EXACT path has already failed — a chunk with a
357
+ // usable canonical identity has a content-addressed handle on its own bytes
358
+ // and needs no estimator at all — so this is honest degradation, not extra
359
+ // work on regions that already resolved. The candidates are the segment's
360
+ // two EDGE sub-spans at the write side's own unit scale (W) — the scale
361
+ // `canonicalWindows` interns, and the only one at which a sub-span could
362
+ // carry a stored identity; a segment of W or less has no interior at all.
363
+ // Edges because a content cut lands INSIDE a unit, so the remnant it split
364
+ // sits against the cut: `steel` is cut after `stee`. Offering every
365
+ // interior offset instead was measured and is worse — it re-anchors
366
+ // segments on spans no boundary ever separated, and broke three of
367
+ // test/17's vote-distribution and root-count assertions (428/1 → 424/5).
368
+ //
369
+ // Selection is by the SAME quantity the region's vote is weighted by —
370
+ // score² · idf — never by score alone: the scaffolding window `is i`
371
+ // resonates at 0.832, far above `stee`, and is worth nothing because its
372
+ // reach is the whole corpus. Nothing new is being measured here; the
373
+ // choice the code did not previously make is made with the criterion it
374
+ // already uses. The region's SPAN narrows with its gist, so breadth,
375
+ // clusters and cross-region pairing all see where the evidence really sits.
376
+ if (!canonicalUsable && chunk && !known && end - start > W) {
377
+ const weigh = (h) => {
378
+ const r = edgeAncestors(ctx, h.id, N, reachMemo);
379
+ if (r.saturated || r.roots.length === 0)
380
+ return null;
381
+ const idf = Math.log(N / Math.max(1, r.contextsReached));
382
+ if (idf <= 0)
383
+ return null;
384
+ return { w: h.score * h.score * idf, id: h.id };
385
+ };
386
+ // The whole-segment candidate reuses the ranking the region needs
387
+ // anyway, so only the two edge probes are new work.
388
+ const scoreOf = async (gist) => {
389
+ const h = await ctx.store.resonate(gist, 1);
390
+ return h.length === 0 ? null : weigh(h[0]);
391
+ };
392
+ const h0 = await ensureHits();
393
+ let best = h0.length > 0 ? weigh(h0[0]) : null;
394
+ let bestSpan = null;
395
+ for (const s0 of [start, end - W]) {
396
+ const sub = gistOf(ctx, query.subarray(s0, s0 + W));
397
+ const cand = await scoreOf(sub);
398
+ if (cand !== null && (best === null || cand.w > best.w)) {
399
+ best = cand;
400
+ bestSpan = [s0, s0 + W, sub];
401
+ }
402
+ }
403
+ if (bestSpan !== null) {
404
+ [start, end, v] = bestSpan;
405
+ hits = null; // the whole-segment ranking no longer describes this span
406
+ canonicalId = canonicalChunkId(ctx, query.subarray(start, end), N, reachMemo);
407
+ canonicalUsable = canonicalId !== null &&
408
+ (ctx.store.hasParents(canonicalId) ||
409
+ ctx.store.hasContainers(canonicalId));
410
+ }
411
+ }
276
412
  const canonicalFailed = chunk && canonicalId === null;
277
413
  let voterId;
278
414
  let score;
@@ -539,6 +675,9 @@ export function poolVotes(ctx, regionVotes, sat, N, td) {
539
675
  saturationMaskedVotes: regionVotes.length - eligible.length,
540
676
  };
541
677
  }
678
+ // The one hub bound (traverse.ts) — N here IS corpusN, threaded down from
679
+ // computeAttention. Read once, not per rule application.
680
+ const bound = hubBound(ctx);
542
681
  const key = (it) => it.kind === "region"
543
682
  ? `r${it.ri}`
544
683
  : it.kind === "anchor"
@@ -569,13 +708,14 @@ export function poolVotes(ctx, regionVotes, sat, N, td) {
569
708
  // region's vote across its FULL corpus-sized fan-in yields O(corpus)
570
709
  // rule applications per region and near-zero per-target weight anyway.
571
710
  // Cap the redistribution at the first √N contexts (insertion order,
572
- // the same convention chooseNext caps by).
573
- const hubBound = Math.ceil(Math.sqrt(N));
711
+ // the same convention chooseNext caps by). Hoisted out of the
712
+ // generator: `rules` is invoked once per popped item, and this used to
713
+ // re-derive the bound on every one of them.
574
714
  for (const r of rv.roots) {
575
715
  // CAPPED read: only the first hubBound targets are ever credited, so
576
716
  // only they are read — a common continuation's full reverse fan-in
577
717
  // is corpus-sized and is never materialised.
578
- const pv = ctx.store.prevFirst(r, hubBound);
718
+ const pv = ctx.store.prevFirst(r, bound);
579
719
  const isAnswer = pv.length > 0 && !ctx.store.hasNext(r);
580
720
  const targets = isAnswer ? pv : [r];
581
721
  for (const t of targets) {
@@ -619,7 +759,15 @@ export function poolVotes(ctx, regionVotes, sat, N, td) {
619
759
  const rv = regionVotes[p0.ri];
620
760
  breadthSum += rv.absorbed ?? 1;
621
761
  premises.push({ kind: "form", span: [rv.start, rv.end] });
622
- spans.push([rv.start, rv.end]);
762
+ // A vote knows where its own evidence sits: `parts` when it stands on
763
+ // several separate places (a joint binding), the merged span
764
+ // otherwise. See RegionVote.parts.
765
+ if (rv.parts !== undefined) {
766
+ for (const [s, e] of rv.parts)
767
+ spans.push([s, e]);
768
+ }
769
+ else
770
+ spans.push([rv.start, rv.end]);
623
771
  }
624
772
  regionSupport.set(pc.item.id, breadthSum);
625
773
  regionSpans.set(pc.item.id, spans);
@@ -867,15 +1015,40 @@ function sameRoots(a, b) {
867
1015
  }
868
1016
  export function canonicalChunkId(ctx, regionBytes, N, reachMemo) {
869
1017
  const len = Math.min(regionBytes.length, ctx.space.maxGroup);
1018
+ // WHICH window anchors a region is decided by reach, not by position. This
1019
+ // used to return at the FIRST offset that matched, which was indistinguishable
1020
+ // from correct while every region was exactly W bytes — there was only one
1021
+ // offset. A content-defined segment is longer, and its first window is
1022
+ // whatever happens to start it: for "is frigi" that is " is ", pure
1023
+ // scaffolding, which reaches every context, saturates, and makes the whole
1024
+ // region ABSTAIN. The region's own content ("frigi") never got a say, and
1025
+ // CAST lost a point of attention it needed (test/29 D1/D2).
1026
+ //
1027
+ // So scan every offset and prefer an anchor that still discriminates: not
1028
+ // saturated, and among those the one reaching the FEWEST contexts (§2.7,
1029
+ // corpus-global). Only when every window in the region saturates does the
1030
+ // old generalising choice stand — there is then no discriminative anchor to
1031
+ // find, and abstaining is the honest outcome.
1032
+ let discId = null;
1033
+ let discReached = Infinity;
1034
+ let fallback = null;
870
1035
  for (let off = 0; off + len <= regionBytes.length; off++) {
871
1036
  const ids = leafIdRun(ctx, regionBytes, off, off + len);
1037
+ // An unknown byte disqualifies THIS window, not the region. This used to
1038
+ // abandon the whole region on the first unseen byte, which was
1039
+ // indistinguishable from correct while regions were exactly W bytes — there
1040
+ // was one window, so failing it was failing the region. A content-defined
1041
+ // segment holds several windows, and a single unknown byte near its start
1042
+ // was silently costing the region its anchor entirely.
872
1043
  if (ids === null)
873
- return null;
1044
+ continue;
874
1045
  const flatId = ctx.store.findBranch(ids);
875
1046
  if (flatId === null)
876
1047
  continue;
877
1048
  if (len < 2)
878
1049
  return flatId;
1050
+ // Within one window, the widest reach is still the right CANONICAL
1051
+ // identity — a chunk's anchor should be its most general stable form.
879
1052
  let bestId = flatId;
880
1053
  let bestReach = edgeAncestors(ctx, flatId, N, reachMemo);
881
1054
  for (let k2 = 1; k2 < len; k2++) {
@@ -890,9 +1063,18 @@ export function canonicalChunkId(ctx, regionBytes, N, reachMemo) {
890
1063
  bestReach = shortReach;
891
1064
  }
892
1065
  }
893
- return bestId;
1066
+ if (fallback === null)
1067
+ fallback = bestId;
1068
+ if (!bestReach.saturated && bestReach.contextsReached < discReached) {
1069
+ discId = bestId;
1070
+ discReached = bestReach.contextsReached;
1071
+ // Nothing can discriminate better than reaching ONE context, so the scan
1072
+ // stops there rather than pricing the rest of the segment's windows.
1073
+ if (discReached <= 1)
1074
+ break;
1075
+ }
894
1076
  }
895
- return null;
1077
+ return discId ?? fallback;
896
1078
  }
897
1079
  export function naturalBreak(votes) {
898
1080
  if (votes.length <= 1)
@@ -1079,6 +1261,15 @@ function betterProposal(a, b) {
1079
1261
  * ANN-query each, merge proposals by candidate id, and validate the winner
1080
1262
  * through the SAME structural gates every other tier answers to (saturation,
1081
1263
  * roots, IDF, contrastive margin). Returns null when nothing survives. */
1264
+ /** {@link structuralResonance}, charged to its own profiling phase — it is
1265
+ * the halo-mediated arm of the cross-region ladder and the one part of it
1266
+ * that resonates. */
1267
+ async function meteredStructuralResonance(...args) {
1268
+ const ctx = args[0];
1269
+ return ctx.meter
1270
+ ? await ctx.meter.time("climb.structuralResonance", () => structuralResonance(...args))
1271
+ : await structuralResonance(...args);
1272
+ }
1082
1273
  export async function structuralResonance(ctx, query, ra, rb, sides, siblingGistMemo, k, N, reachMemo,
1083
1274
  /** Each side's OWN individual climb roots (from voteRegions), when it cast
1084
1275
  * one — the self-evidence backstop structural-resonance needs and the
@@ -1243,7 +1434,19 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1243
1434
  // one side is always individually discriminative.
1244
1435
  //
1245
1436
  // Only MAXIMAL spans compose: a span contained in another candidate is a
1246
- // fragment of that candidate's evidence, never independent of it.
1437
+ // fragment of that candidate's evidence, never independent of it — but
1438
+ // containment alone does not establish that relation. An APPROXIMATE
1439
+ // container (a fold segment whose gist merely resonated) does not hold the
1440
+ // evidence of an EXACT one (a recognised site, content-addressed): its
1441
+ // bytes straddle the site rather than explain it, so calling the site a
1442
+ // fragment of it discards the only exact reading of those bytes. Measured:
1443
+ // on `blue then square` the segment `blue t` swallowed the site `blue`,
1444
+ // leaving one candidate and no pair, while on `red then circle` the
1445
+ // segment happened to end at `red `'s edge and the same query shape
1446
+ // composed — the outcome turned on where a cut fell. This is the same
1447
+ // discipline the between-region gate below already states: an approximate
1448
+ // region climbing "somewhere" is ordinary noise, not evidence.
1449
+ //
1247
1450
  // Shared across every cross-region probe in this climb: a sibling
1248
1451
  // successfully reconstructed while probing one pair must not be read and
1249
1452
  // perceived again while probing another pair in the same climb.
@@ -1268,7 +1471,8 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1268
1471
  const cand = eligible.filter((x) => !eligible.some((y) => y !== x &&
1269
1472
  regions[y].start <= regions[x].start &&
1270
1473
  regions[x].end <= regions[y].end &&
1271
- regions[y].end - regions[y].start > regions[x].end - regions[x].start));
1474
+ regions[y].end - regions[y].start > regions[x].end - regions[x].start &&
1475
+ (regions[y].known || !regions[x].known)));
1272
1476
  const none = { votes: [], superseded: new Set() };
1273
1477
  if (td) {
1274
1478
  td.crossRegionSummary = {
@@ -1496,7 +1700,7 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1496
1700
  }
1497
1701
  const ownRootsA = rvs.votes.find((v) => v.start === ra.start && v.end === ra.end)?.roots;
1498
1702
  const ownRootsB = rvs.votes.find((v) => v.start === rb.start && v.end === rb.end)?.roots;
1499
- structuralPick = await structuralResonance(ctx, query, ra, rb, sides, siblingGistMemo, k, N, reachMemo, ownRootsA, ownRootsB, resonanceTrace);
1703
+ structuralPick = await meteredStructuralResonance(ctx, query, ra, rb, sides, siblingGistMemo, k, N, reachMemo, ownRootsA, ownRootsB, resonanceTrace);
1500
1704
  }
1501
1705
  if (structuralPick === null) {
1502
1706
  pushProbe(reasons.length > 0 ? "resonance-ineligible" : "resonance-rejected");
@@ -1646,10 +1850,27 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1646
1850
  spanStart = Math.min(spanStart, regions[ei].start);
1647
1851
  spanEnd = Math.max(spanEnd, regions[ei].end);
1648
1852
  }
1649
- consumed.add(cand[a]);
1650
- consumed.add(cand[b]);
1651
- for (const ei of bestExtras)
1652
- consumed.add(ei);
1853
+ // CONSUMPTION IS FOR CONTAINER-BACKED EVIDENCE ONLY. Consuming a
1854
+ // candidate says "its evidence is already composed at full joint
1855
+ // strength, re-pairing it would vote the same container twice" — a
1856
+ // claim only a real container can make. A structural-resonance pick
1857
+ // has none (see above: it is NOT a Junction), so consuming its
1858
+ // endpoints locks up candidates on the strength of an ANN guess and
1859
+ // stops genuine evidence from ever composing them. Measured: on
1860
+ // `greet reply-greet then red then circle` the pair
1861
+ // `reply-greet` ▸ `red` resonated to `red square` and consumed `red`,
1862
+ // after which `red` ▸ `circle` was never probed and the exact junction
1863
+ // `red circle` — a stored whole, sitting right there — went unfound.
1864
+ // This is spec §15's asymmetry (only exact DAG evidence may explain
1865
+ // ordinary votes away) applied to the other way a tier can silence
1866
+ // evidence. Both votes now stand and pooling decides between them,
1867
+ // which is what the mechanism market is for.
1868
+ if (structuralPick === null) {
1869
+ consumed.add(cand[a]);
1870
+ consumed.add(cand[b]);
1871
+ for (const ei of bestExtras)
1872
+ consumed.add(ei);
1873
+ }
1653
1874
  // EXPLAINING AWAY — see the block comment above the function. Byte
1654
1875
  // containment in the joint container is the relatedness test (the
1655
1876
  // vote's bytes are literally part of the learnt whole), and FULL root
@@ -1697,6 +1918,16 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1697
1918
  w,
1698
1919
  wFocus: w,
1699
1920
  absorbed: 1 + explainedAway,
1921
+ // The places this junction actually stands on — its two endpoints and
1922
+ // any N-ary extras, NOT the merged span [spanStart, spanEnd], which
1923
+ // swallows the gap and reads as one neighbourhood. See
1924
+ // RegionVote.parts.
1925
+ parts: [cand[a], cand[b], ...bestExtras]
1926
+ .map((ri) => [
1927
+ regions[ri].start,
1928
+ regions[ri].end,
1929
+ ])
1930
+ .sort((x, y) => x[0] - y[0]),
1700
1931
  });
1701
1932
  pushProbe("accepted");
1702
1933
  if (td) {
@@ -26,5 +26,14 @@ export interface BridgeHit {
26
26
  export declare function dismissedKnownContent(ctx: MindContext, query: Uint8Array, spans: ReadonlyArray<readonly [number, number]>): boolean;
27
27
  /** Recall's corroborated-substitution bridge — see the module comment.
28
28
  * Returns the best bridged grounding proposal, or null. */
29
- export declare function substitutionBridge(ctx: MindContext, query: Uint8Array, proposed?: ReadonlyArray<number>): Promise<BridgeHit | null>;
29
+ /** `proposed` is a THUNK, not a list: the bridge's own cheap gates (the
30
+ * two-quantum query floor and the O(|query|) stored-window anchor scan)
31
+ * decide whether ANY candidate can be aligned, and they need no proposals
32
+ * to do it. Resolving the caller's proposals eagerly meant recall paid its
33
+ * exhaustive whole-index resonance — the most expensive single act on the
34
+ * refusal path — for every query, including the ones whose windows the
35
+ * store has never seen and which the anchor scan rejects outright. Same
36
+ * investment discipline the mechanism floors follow (AGENTS §2.6): never
37
+ * compute a shared analysis just to discard it. */
38
+ export declare function substitutionBridge(ctx: MindContext, query: Uint8Array, proposed?: () => Promise<ReadonlyArray<number>>): Promise<BridgeHit | null>;
30
39
  export {};