@hviana/sema 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/package.json +1 -1
  2. package/src/geometry.ts +52 -3
  3. package/src/mind/attention.ts +1469 -120
  4. package/src/mind/index.ts +16 -0
  5. package/src/mind/junction.ts +125 -16
  6. package/src/mind/mind.ts +15 -0
  7. package/src/mind/rationale.ts +12 -4
  8. package/src/mind/traverse.ts +75 -7
  9. package/src/mind/types.ts +25 -0
  10. package/test/51-structural-resonance-ladder.test.mjs +552 -0
  11. package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
  12. package/test/53-cross-region-probe-instrumentation.test.mjs +356 -0
  13. package/dist/example/demo.d.ts +0 -1
  14. package/dist/example/demo.js +0 -39
  15. package/dist/example/train_base.d.ts +0 -87
  16. package/dist/example/train_base.js +0 -2252
  17. package/dist/src/alphabet.d.ts +0 -7
  18. package/dist/src/alphabet.js +0 -33
  19. package/dist/src/alu/src/alu.d.ts +0 -185
  20. package/dist/src/alu/src/alu.js +0 -440
  21. package/dist/src/alu/src/expr.d.ts +0 -61
  22. package/dist/src/alu/src/expr.js +0 -318
  23. package/dist/src/alu/src/index.d.ts +0 -11
  24. package/dist/src/alu/src/index.js +0 -19
  25. package/dist/src/alu/src/kernel-arith.d.ts +0 -16
  26. package/dist/src/alu/src/kernel-arith.js +0 -264
  27. package/dist/src/alu/src/kernel-bits.d.ts +0 -19
  28. package/dist/src/alu/src/kernel-bits.js +0 -152
  29. package/dist/src/alu/src/kernel-logic.d.ts +0 -4
  30. package/dist/src/alu/src/kernel-logic.js +0 -60
  31. package/dist/src/alu/src/kernel-nd.d.ts +0 -3
  32. package/dist/src/alu/src/kernel-nd.js +0 -208
  33. package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
  34. package/dist/src/alu/src/kernel-numeric.js +0 -366
  35. package/dist/src/alu/src/operation.d.ts +0 -168
  36. package/dist/src/alu/src/operation.js +0 -189
  37. package/dist/src/alu/src/parser.d.ts +0 -221
  38. package/dist/src/alu/src/parser.js +0 -577
  39. package/dist/src/alu/src/resonance.d.ts +0 -55
  40. package/dist/src/alu/src/resonance.js +0 -126
  41. package/dist/src/alu/src/text.d.ts +0 -31
  42. package/dist/src/alu/src/text.js +0 -73
  43. package/dist/src/alu/src/value.d.ts +0 -109
  44. package/dist/src/alu/src/value.js +0 -300
  45. package/dist/src/alu/test/alu.test.d.ts +0 -1
  46. package/dist/src/alu/test/alu.test.js +0 -764
  47. package/dist/src/bytes.d.ts +0 -14
  48. package/dist/src/bytes.js +0 -59
  49. package/dist/src/canon.d.ts +0 -26
  50. package/dist/src/canon.js +0 -57
  51. package/dist/src/config.d.ts +0 -111
  52. package/dist/src/config.js +0 -91
  53. package/dist/src/derive/src/deduction.d.ts +0 -125
  54. package/dist/src/derive/src/deduction.js +0 -155
  55. package/dist/src/derive/src/index.d.ts +0 -7
  56. package/dist/src/derive/src/index.js +0 -11
  57. package/dist/src/derive/src/priority-queue.d.ts +0 -20
  58. package/dist/src/derive/src/priority-queue.js +0 -73
  59. package/dist/src/derive/src/rewrite.d.ts +0 -56
  60. package/dist/src/derive/src/rewrite.js +0 -100
  61. package/dist/src/derive/src/trie.d.ts +0 -90
  62. package/dist/src/derive/src/trie.js +0 -217
  63. package/dist/src/derive/test/derive.test.d.ts +0 -1
  64. package/dist/src/derive/test/derive.test.js +0 -122
  65. package/dist/src/extension.d.ts +0 -37
  66. package/dist/src/extension.js +0 -7
  67. package/dist/src/geometry.d.ts +0 -176
  68. package/dist/src/geometry.js +0 -546
  69. package/dist/src/index.d.ts +0 -16
  70. package/dist/src/index.js +0 -18
  71. package/dist/src/ingest-cache.d.ts +0 -41
  72. package/dist/src/ingest-cache.js +0 -161
  73. package/dist/src/mind/articulation.d.ts +0 -6
  74. package/dist/src/mind/articulation.js +0 -99
  75. package/dist/src/mind/attention.d.ts +0 -88
  76. package/dist/src/mind/attention.js +0 -997
  77. package/dist/src/mind/bridge.d.ts +0 -30
  78. package/dist/src/mind/bridge.js +0 -569
  79. package/dist/src/mind/canonical.d.ts +0 -29
  80. package/dist/src/mind/canonical.js +0 -88
  81. package/dist/src/mind/graph-search.d.ts +0 -285
  82. package/dist/src/mind/graph-search.js +0 -956
  83. package/dist/src/mind/index.d.ts +0 -5
  84. package/dist/src/mind/index.js +0 -5
  85. package/dist/src/mind/junction.d.ts +0 -95
  86. package/dist/src/mind/junction.js +0 -262
  87. package/dist/src/mind/learning.d.ts +0 -47
  88. package/dist/src/mind/learning.js +0 -248
  89. package/dist/src/mind/match.d.ts +0 -126
  90. package/dist/src/mind/match.js +0 -441
  91. package/dist/src/mind/mechanisms/alu.d.ts +0 -4
  92. package/dist/src/mind/mechanisms/alu.js +0 -36
  93. package/dist/src/mind/mechanisms/cast.d.ts +0 -89
  94. package/dist/src/mind/mechanisms/cast.js +0 -716
  95. package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
  96. package/dist/src/mind/mechanisms/confluence.js +0 -225
  97. package/dist/src/mind/mechanisms/cover.d.ts +0 -6
  98. package/dist/src/mind/mechanisms/cover.js +0 -203
  99. package/dist/src/mind/mechanisms/extraction.d.ts +0 -67
  100. package/dist/src/mind/mechanisms/extraction.js +0 -387
  101. package/dist/src/mind/mechanisms/recall.d.ts +0 -13
  102. package/dist/src/mind/mechanisms/recall.js +0 -325
  103. package/dist/src/mind/mind.d.ts +0 -274
  104. package/dist/src/mind/mind.js +0 -572
  105. package/dist/src/mind/pipeline-mechanism.d.ts +0 -142
  106. package/dist/src/mind/pipeline-mechanism.js +0 -213
  107. package/dist/src/mind/pipeline.d.ts +0 -20
  108. package/dist/src/mind/pipeline.js +0 -217
  109. package/dist/src/mind/primitives.d.ts +0 -70
  110. package/dist/src/mind/primitives.js +0 -287
  111. package/dist/src/mind/rationale.d.ts +0 -134
  112. package/dist/src/mind/rationale.js +0 -162
  113. package/dist/src/mind/reasoning.d.ts +0 -34
  114. package/dist/src/mind/reasoning.js +0 -240
  115. package/dist/src/mind/recognition.d.ts +0 -20
  116. package/dist/src/mind/recognition.js +0 -390
  117. package/dist/src/mind/resonance.d.ts +0 -23
  118. package/dist/src/mind/resonance.js +0 -0
  119. package/dist/src/mind/trace.d.ts +0 -15
  120. package/dist/src/mind/trace.js +0 -73
  121. package/dist/src/mind/traverse.d.ts +0 -113
  122. package/dist/src/mind/traverse.js +0 -504
  123. package/dist/src/mind/types.d.ts +0 -271
  124. package/dist/src/mind/types.js +0 -130
  125. package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
  126. package/dist/src/rabitq-ivf/src/database.js +0 -201
  127. package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
  128. package/dist/src/rabitq-ivf/src/index.js +0 -4
  129. package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
  130. package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
  131. package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
  132. package/dist/src/rabitq-ivf/src/prng.js +0 -36
  133. package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
  134. package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
  135. package/dist/src/sema.d.ts +0 -31
  136. package/dist/src/sema.js +0 -63
  137. package/dist/src/store-sqlite.d.ts +0 -184
  138. package/dist/src/store-sqlite.js +0 -942
  139. package/dist/src/store.d.ts +0 -663
  140. package/dist/src/store.js +0 -1618
  141. package/dist/src/vec.d.ts +0 -31
  142. package/dist/src/vec.js +0 -109
@@ -1,997 +0,0 @@
1
- // attention.ts — Consensus climb / attention pipeline (Section 4 of the mind).
2
- //
3
- // Every region of the query's perceived tree casts a resonance vote for the
4
- // context (learnt fact) it best climbs to. Votes are pooled through the very
5
- // deduction engine (lightestDerivation) that GraphSearch covers with — so a
6
- // pooled-evidence decision is one weighted rule of the SAME deduction system,
7
- // not a hand-rolled tally. The result is one or more independent points of
8
- // attention for the rest of the pipeline to follow.
9
- import { isChunk } from "../sema.js";
10
- import { lightestDerivation, } from "../derive/src/index.js";
11
- import { consensusFloor, dominates, estimatorNoise } from "../geometry.js";
12
- import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
13
- import { recognise } from "./recognition.js";
14
- import { leafIdRun } from "./canonical.js";
15
- import { corpusN, edgeAncestors } from "./traverse.js";
16
- import { cachedRead, junctionContainersFrom, junctionSeeds, junctionSynonyms, walkCache, } from "./junction.js";
17
- import { indexOf } from "../bytes.js";
18
- import { rItem, rNode, traceDerivation } from "./trace.js";
19
- // ── Public entry points ───────────────────────────────────────────────────
20
- /** Climb the query's perceived byte regions up the structural DAG via
21
- * resonance, pool the evidence, and return only the ROOT points of
22
- * attention — those that cleared commitVotes' significance floor. */
23
- export async function climbAttention(ctx, query, k, mode = "inverse") {
24
- return (await climbAttentionAll(ctx, query, k, mode)).roots;
25
- }
26
- /** Full read-out of one consensus climb: both the roots (dominant points of
27
- * attention) and the entire ranked list. Cached via ctx.climbMemo, ALWAYS —
28
- * see {@link recognise} for why this memo (and recognise()'s own) must
29
- * never be skipped while tracing: computeAttention's collectRegions walks
30
- * the query's perceived tree via the same foldTree whose subtree-resolution
31
- * fast path makes a second call on identical bytes non-idempotent once
32
- * ctx._resolvedSubtrees is warm (which a multi-turn conversation's shared
33
- * prefix subtrees guarantee by the second turn). A cache hit still emits
34
- * a trace step — abbreviated, since the full per-sub-region voting detail
35
- * {@link traceAttention} builds isn't preserved by the cached read-out —
36
- * so a traced response is never silently blacked out for a repeated
37
- * query. */
38
- export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
39
- // Content-keyed memo — works for both single-turn respond() and multi-turn
40
- // respondTurn().
41
- if (ctx.climbMemo) {
42
- const contentKey = latin1Key(query);
43
- const modeKey = `${k}:${mode}`;
44
- let byRead = ctx.climbMemo.get(contentKey);
45
- if (byRead === undefined) {
46
- ctx.climbMemo.set(contentKey, byRead = new Map());
47
- }
48
- const hit = byRead.get(modeKey);
49
- if (hit !== undefined) {
50
- 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 — ` +
51
- `${hit.roots.length} point(s) of attention`);
52
- return hit;
53
- }
54
- const read = await computeAttention(ctx, query, k, mode);
55
- byRead.set(modeKey, read);
56
- return read;
57
- }
58
- return computeAttention(ctx, query, k, mode);
59
- }
60
- // ── Pipeline ──────────────────────────────────────────────────────────────
61
- export async function computeAttention(ctx, query, k, mode) {
62
- const regions = collectRegions(ctx, query);
63
- // Recognised sites carry structural evidence that perceived sub-regions
64
- // miss: a word crossing a W-boundary is split into chunks whose partial
65
- // gists may not resonate distinctively, but the SITE (content-addressed,
66
- // exact) names the whole form. Adding sites as climb regions lets the
67
- // consensus vote with the full word, at zero cost — recognition is already
68
- // memoised per response (ctx.recogniseMemo), and gistOf for short sites is
69
- // O(|span|·D). Sites that overlap perceived regions add corroborating
70
- // evidence; sites in gaps (like cross-boundary words) fill them.
71
- const rec = recognise(ctx, query);
72
- for (const s of rec.sites) {
73
- regions.push({
74
- v: gistOf(ctx, query.subarray(s.start, s.end)),
75
- start: s.start,
76
- end: s.end,
77
- chunk: false,
78
- known: true, // a recognised site IS a stored form
79
- });
80
- }
81
- if (regions.length === 0)
82
- return { roots: [], ranked: [] };
83
- const N = corpusN(ctx);
84
- // One climb per distinct anchor for the WHOLE query: regions sharing a
85
- // chunk, and canonicalChunkId's prefix probes, all hit this memo instead of
86
- // re-reading the anchor's full edge fan-out from the store.
87
- const reachMemo = new Map();
88
- const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo);
89
- // ── Cross-region: DIRECT region-to-region interaction ─────────────────
90
- // Two regions whose individual climbs land on DIFFERENT contexts leave
91
- // their JOINT context — the learnt whole that contains BOTH — with no
92
- // vote. crossRegionVotes recovers it by the bridge's content-addressed
93
- // junction ascent (see the note above the function).
94
- const cross = await crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo);
95
- // A vote SUPERSEDED by exact joint evidence (its bytes literally live
96
- // inside the joint container, yet it climbed elsewhere — grid aliasing)
97
- // is dropped, not down-weighted: the joint container explains it away.
98
- const allVotes = cross.votes.length > 0
99
- ? [
100
- ...rvs.votes.filter((v) => !cross.superseded.has(v)),
101
- ...cross.votes,
102
- ]
103
- : rvs.votes;
104
- // ──────────────────────────────────────────────────────────────────────
105
- if (allVotes.length === 0) {
106
- traceAttention(ctx, regions, rvs.voters, []);
107
- return { roots: [], ranked: [] };
108
- }
109
- const sat = detectSaturated(ctx, regions, rvs.saturated);
110
- const pooled = poolVotes(ctx, allVotes, sat, N);
111
- return commitVotes(ctx, pooled, sat, regions, rvs.voters, N);
112
- }
113
- export function collectRegions(ctx, query) {
114
- const regions = [];
115
- // A region that DOMINATES the query (covers more than half — the shared
116
- // {@link dominates} test liftAnswer uses for a span that swallows its
117
- // surroundings) can never itself discriminate between several topics the
118
- // query weaves; voting with it only when it is the sole structure (no
119
- // narrower region exists) keeps a flat/short query's single point of
120
- // attention intact without letting a broad, non-discriminative wrapper
121
- // dilute a multi-topic query's vote or masquerade as a genuine second
122
- // point of attention.
123
- // foldTree (not walkTree): the same post-order walk, but each node also
124
- // resolves content-addressed against the store — `known` is what lets the
125
- // climb keep exact evidence at full weight while margin-damping the
126
- // approximate kind (see voteRegions). One findLeaf/findBranch per tree
127
- // node, the same lookups a deposit pays.
128
- foldTree(ctx, perceive(ctx, query), 0, (n, start, end, node) => {
129
- if (n.kids === null)
130
- return;
131
- if (!dominates(end - start, query.length) || regions.length === 0) {
132
- regions.push({
133
- v: n.v,
134
- start,
135
- end,
136
- chunk: isChunk(n),
137
- known: node !== null,
138
- });
139
- }
140
- });
141
- return regions;
142
- }
143
- export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
144
- const regionSaturated = new Array(regions.length).fill(false);
145
- const regionVotes = [];
146
- const regionVoter = ctx.trace ? regions.map(() => null) : [];
147
- for (let ri = 0; ri < regions.length; ri++) {
148
- const { v, start, end, chunk, known } = regions[ri];
149
- // EXACT-FIRST: a chunk whose canonical anchor is content-addressed needs
150
- // no estimator — identity is exact, so its score is 1 BY DEFINITION (the
151
- // estimated cosine of a form with itself, minus quantisation noise, and
152
- // the caveat atop geometry.ts forbids trusting the estimate over the
153
- // exact resolution anyway). The ANN query is deferred behind
154
- // `ensureHits` and paid only when actually consulted: the orphan
155
- // fallback, the contrastive margin (approximate regions only), or a
156
- // region with no usable canonical. On chunk-heavy queries this removes
157
- // the resonate() call for most exact regions — the single largest
158
- // remaining inference sink — with the anchor choice unchanged (the
159
- // canonical branch already ignored hits[0]).
160
- const canonicalId = chunk
161
- ? canonicalChunkId(ctx, query.subarray(start, end), N, reachMemo)
162
- : null;
163
- const canonicalUsable = canonicalId !== null &&
164
- (ctx.store.hasParents(canonicalId) ||
165
- ctx.store.hasContainers(canonicalId));
166
- let hits = null;
167
- const ensureHits = async () => hits ??= await ctx.store.resonate(v, k);
168
- const canonicalFailed = chunk && canonicalId === null;
169
- let voterId;
170
- let score;
171
- let scoreId; // the node the score was measured against
172
- if (canonicalUsable) {
173
- voterId = canonicalId;
174
- score = 1;
175
- scoreId = canonicalId;
176
- }
177
- else {
178
- const h = await ensureHits();
179
- if (h.length === 0)
180
- continue;
181
- voterId = h[0].id;
182
- score = h[0].score;
183
- scoreId = h[0].id;
184
- }
185
- let reach = edgeAncestors(ctx, voterId, N, reachMemo);
186
- // A region's vote must not die with the TOP hit: `hits[1..k]` were
187
- // already fetched, and the top-ranked anchor being a structural orphan
188
- // (no edge-bearing ancestors) is an accident of the approximate ranking,
189
- // not evidence the region relates to nothing. Walk the remaining hits —
190
- // nearest first, climbs memoised — until one climbs. A SATURATED reach
191
- // is not an orphan: it is a deliberate abstention, kept as-is.
192
- if (reach.roots.length === 0 && !reach.saturated) {
193
- for (const h of await ensureHits()) {
194
- if (h.id === voterId)
195
- continue;
196
- const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
197
- if (r2.saturated || r2.roots.length > 0) {
198
- 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");
199
- reach = r2;
200
- voterId = h.id;
201
- score = h.score;
202
- scoreId = h.id;
203
- break;
204
- }
205
- }
206
- }
207
- else if (!canonicalUsable && reach.saturated) {
208
- // TIE-BAND saturation fallback. A saturated top hit abstains the whole
209
- // region (a hub's reach concludes nothing) — but the hub may only CLAIM
210
- // that abstention when it is DISTINGUISHABLY the nearest anchor. The
211
- // resonance ranking is an estimate: the difference between two scores
212
- // against the same query carries √2× the estimator's per-score error,
213
- // ≈ 1/√D ({@link estimatorNoise}) — so any hit within that band of the
214
- // top is the SAME rank at measurement resolution, and letting the hub
215
- // win the tie decides the region by quantisation accident (observed:
216
- // a 0.1σ rank inversion flipped a pinned behaviour when the query
217
- // estimator sharpened from 4 to 8 bits). Walk the tied hits, nearest
218
- // first; the first that climbs somewhere non-saturated votes for the
219
- // region. Beyond the band the hub is genuinely nearest and its
220
- // abstention stands. A KNOWN (content-addressed) region never enters:
221
- // its anchor is exact, not an estimate.
222
- const band = estimatorNoise(ctx.store.D);
223
- for (const h of await ensureHits()) {
224
- if (h.id === voterId)
225
- continue;
226
- if (h.score < score - band)
227
- break; // hits are nearest-first
228
- const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
229
- if (!r2.saturated && r2.roots.length > 0) {
230
- 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");
231
- reach = r2;
232
- voterId = h.id;
233
- score = h.score;
234
- scoreId = h.id;
235
- break;
236
- }
237
- }
238
- }
239
- regionSaturated[ri] = reach.saturated;
240
- if (reach.roots.length === 0)
241
- continue;
242
- if (reach.saturated)
243
- continue;
244
- // One IDF per region — dfWeight() and the focus weight used to compute
245
- // the same logarithm independently.
246
- const idf = Math.log(N / Math.max(1, reach.contextsReached));
247
- const df = Math.log(1 + reach.contextsReached);
248
- const wf = mode === "direct" ? df : mode === "combined" ? idf + df : idf;
249
- if (wf <= 0)
250
- continue;
251
- // CONTRASTIVE-MARGIN GATE — the compensation the linear (byte-proportional)
252
- // fold demands, applied to APPROXIMATE evidence only. Under the linear
253
- // fold a resonance score reads "fraction of aligned shared bytes", so a
254
- // NOVEL span sharing a frame with several stored exemplars scores high
255
- // against each of them without being evidence of ANY of them: the shared
256
- // scaffolding, not the span's own content, carries the similarity. Such a
257
- // frame region resonates ~equally to every framed exemplar, so its top hit
258
- // barely beats the best DIFFERENT-conclusion rival (a different climb
259
- // root-set) — its discriminative margin, score MINUS that rival, collapses
260
- // toward zero. A region votes only when that margin clears the estimator's
261
- // own noise floor (1/√D — see {@link estimatorNoise}); below it the margin
262
- // is quantisation noise, not evidence. A KNOWN region (content-addressed,
263
- // exact) skips the contrast: it IS learnt content, not an approximation.
264
- //
265
- // The margin GATES; it does NOT scale the weight. A surviving region votes
266
- // at its genuine strength (score²·wf) — the SAME scale {@link
267
- // consensusFloor} is derived for. Using the margin as a MULTIPLIER
268
- // (score·margin) conflated "discriminative" with "strong": a genuinely
269
- // discriminative span whose frame-rival happened to score close got a tiny
270
- // vote, systematically compressing correct scaffolding-dominated groundings
271
- // (reordered / paraphrased queries) below the floor so they grounded
272
- // nothing. Gating at the noise floor keeps frame-echo suppression (a frame
273
- // region's margin ≈ 0 is gated out) without penalising honest evidence.
274
- if (!known) {
275
- let margin = score;
276
- for (const h of await ensureHits()) {
277
- if (h.id === voterId)
278
- continue;
279
- const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
280
- if (r2.saturated || r2.roots.length === 0)
281
- continue; // concludes nothing
282
- if (sameRoots(r2.roots, reach.roots))
283
- continue; // same conclusion
284
- margin = score - h.score; // hits are nearest-first: the best rival
285
- break;
286
- }
287
- if (margin <= estimatorNoise(ctx.store.D))
288
- continue;
289
- }
290
- // MUTUAL-EXPLANATION WEIGHT (angle + magnitude). Under the linear fold
291
- // cos = shared/(‖r‖·‖h‖) with ‖·‖² = content bytes, so the old score²
292
- // was already — implicitly — (shared/len_r)·(shared/len_h): the fraction
293
- // of the REGION the hit explains times the fraction of the HIT the
294
- // region pins down. Made explicit, each factor is computed from the two
295
- // magnitudes (the region's own span; the hit's, read from the store —
296
- // contentLen, √bytes being the linear fold's gist norm) and CAPPED at 1:
297
- // the estimated cosine can imply more shared content than the smaller
298
- // side even holds, and the uncapped square silently credited that
299
- // impossible surplus — a small region echoing inside a large context, or
300
- // the reverse, voted above its physical evidence. In the uncapped
301
- // regime this is exactly score², the scale {@link consensusFloor} is
302
- // derived for. (The margin gate above deliberately stays in raw cosine
303
- // units: it tests the ESTIMATOR's noise floor, which lives in cosine
304
- // space; converting each side by its own hit's magnitude would compare
305
- // noise floors of different scales.)
306
- const lenR = Math.max(1, end - start);
307
- // Cap the magnitude read at lenR·D: past it s/ratio ≤ s/√D — below the
308
- // estimator's own noise floor — so the mutual weight is ~0 regardless
309
- // and the clamped value yields exactly that; no full walk of a huge hit.
310
- const ratio = Math.sqrt(Math.max(1, ctx.store.contentLen(scoreId, lenR * ctx.store.D)) / lenR);
311
- const mutual = Math.min(1, score * ratio) * Math.min(1, score / ratio);
312
- const w = (mutual * wf) / reach.roots.length;
313
- const wFocus = (mutual * idf) / reach.roots.length;
314
- regionVotes.push({
315
- start,
316
- end,
317
- canonicalFailed,
318
- roots: reach.roots,
319
- w,
320
- wFocus,
321
- });
322
- if (ctx.trace) {
323
- regionVoter[ri] = { id: voterId, score, w: wf };
324
- }
325
- }
326
- return {
327
- votes: regionVotes,
328
- saturated: regionSaturated,
329
- voters: regionVoter,
330
- };
331
- }
332
- /** The consensus vote as EVIDENCE POOLING, not shortest path: each surviving
333
- * region is an axiom; it contributes to every root it climbed to (or, for a
334
- * terminal answer node, to the contexts that lead to it) by a `combine:
335
- * "sum"` rule, so independent regions corroborating the same anchor ADD
336
- * rather than compete to be the cheapest route (see {@link Rule.combine} in
337
- * derive/src/deduction.ts). Run through the very engine {@link
338
- * GraphSearch} covers with — `lightestDerivation` — so a pooled-evidence
339
- * decision is, like a followed edge or a spliced connector, one weighted
340
- * rule of the SAME deduction system, not a separate hand-rolled tally that
341
- * merely logs alongside it. `votesIdf`/`support` are the same two
342
- * read-outs {@link commitVotes} always gated on; only how they accumulate
343
- * changed. */
344
- export function poolVotes(ctx, regionVotes, sat, N) {
345
- const eligible = [];
346
- for (let ri = 0; ri < regionVotes.length; ri++) {
347
- const rv = regionVotes[ri];
348
- if (rv.canonicalFailed &&
349
- sat.intervals.some((iv) => rv.start >= iv.start && rv.end <= iv.end)) {
350
- continue;
351
- }
352
- eligible.push(ri);
353
- }
354
- const key = (it) => it.kind === "region"
355
- ? `r${it.ri}`
356
- : it.kind === "anchor"
357
- ? `a${it.id}`
358
- : `x${it.id}`;
359
- const pool = new Map();
360
- const system = {
361
- key,
362
- *axioms() {
363
- for (const ri of eligible) {
364
- yield { item: { kind: "region", ri }, cost: 0 };
365
- }
366
- },
367
- isGoal: () => false, // exhaust every axiom; there is no single goal to stop at
368
- // Every region axiom ties at cost 0, so the agenda's pop order among them
369
- // is otherwise unspecified; ordering by `ri` here only steers the HEAP
370
- // (never added to a stored cost — see relax's use of h) so pooling fires
371
- // in exactly the regionVotes array order the original loop used, byte-for-
372
- // byte reproducing its accumulation and tie-break order.
373
- heuristic: (it) => it.kind === "region" ? it.ri : 0,
374
- *rules(it) {
375
- if (it.kind !== "region")
376
- return;
377
- const rv = regionVotes[it.ri];
378
- // The same hub bound the rest of the system uses (edgeAncestors' parent
379
- // cutoff, chooseNext's candidate cap): a terminal answer followed by
380
- // more than √N contexts is a non-discriminative hub — spreading a
381
- // region's vote across its FULL corpus-sized fan-in yields O(corpus)
382
- // rule applications per region and near-zero per-target weight anyway.
383
- // Cap the redistribution at the first √N contexts (insertion order,
384
- // the same convention chooseNext caps by).
385
- const hubBound = Math.ceil(Math.sqrt(N));
386
- for (const r of rv.roots) {
387
- // CAPPED read: only the first hubBound targets are ever credited, so
388
- // only they are read — a common continuation's full reverse fan-in
389
- // is corpus-sized and is never materialised.
390
- const pv = ctx.store.prevFirst(r, hubBound);
391
- const isAnswer = pv.length > 0 && !ctx.store.hasNext(r);
392
- const targets = isAnswer ? pv : [r];
393
- for (const t of targets) {
394
- yield {
395
- premises: [it],
396
- conclusion: { kind: "anchor", id: t },
397
- cost: rv.w / targets.length,
398
- combine: "sum",
399
- };
400
- yield {
401
- premises: [it],
402
- conclusion: { kind: "anchorFocus", id: t },
403
- cost: rv.wFocus / targets.length,
404
- combine: "sum",
405
- };
406
- }
407
- }
408
- },
409
- pool,
410
- };
411
- lightestDerivation(system);
412
- const votes = new Map();
413
- const votesIdf = new Map();
414
- const support = new Map();
415
- const regionSupport = new Map();
416
- const regionSpans = new Map();
417
- const steps = [];
418
- let order = 0;
419
- for (const pc of pool.values()) {
420
- if (pc.item.kind === "anchor") {
421
- votes.set(pc.item.id, pc.cost);
422
- const premises = [];
423
- const seenRi = new Set();
424
- let breadthSum = 0;
425
- const spans = [];
426
- for (const c of pc.contributions) {
427
- const p0 = c.premises[0].item;
428
- if (p0.kind !== "region" || seenRi.has(p0.ri))
429
- continue;
430
- seenRi.add(p0.ri);
431
- const rv = regionVotes[p0.ri];
432
- breadthSum += rv.absorbed ?? 1;
433
- premises.push({ kind: "form", span: [rv.start, rv.end] });
434
- spans.push([rv.start, rv.end]);
435
- }
436
- regionSupport.set(pc.item.id, breadthSum);
437
- regionSpans.set(pc.item.id, spans);
438
- steps.push({
439
- order: order++,
440
- move: "pool-vote",
441
- premises,
442
- conclusion: { kind: "form", span: [-1, -1], node: pc.item.id },
443
- cost: pc.cost,
444
- producers: [],
445
- });
446
- }
447
- else if (pc.item.kind === "anchorFocus") {
448
- votesIdf.set(pc.item.id, pc.cost);
449
- let bestRv = null;
450
- for (const c of pc.contributions) {
451
- const p0 = c.premises[0].item;
452
- if (p0.kind !== "region")
453
- continue;
454
- const rv = regionVotes[p0.ri];
455
- if (!bestRv || rv.wFocus > bestRv.wFocus)
456
- bestRv = rv;
457
- }
458
- if (bestRv) {
459
- support.set(pc.item.id, {
460
- start: bestRv.start,
461
- end: bestRv.end,
462
- w: bestRv.wFocus,
463
- });
464
- }
465
- }
466
- }
467
- return { votes, votesIdf, support, regionSupport, regionSpans, steps };
468
- }
469
- /** The number of DISTINCT clusters a root's contributing regions form —
470
- * see Attention.clusters. Two regions belong to the same cluster iff the
471
- * gap between them is strictly less than one river-fold quantum W: at
472
- * that distance there is no room for a genuinely separate, independently
473
- * perceivable unit of content between them (the same "smallest meaningful
474
- * distinction" quantum {@link reachThreshold}'s own doc invokes). A gap
475
- * of a full quantum or more means real, separate structure could sit
476
- * between the two spans, so they count as independent corroboration.
477
- * Strict `<` (not `<=`): verified against gap 3.1's own "gender equality"
478
- * root, whose two genuine clusters sit EXACTLY W bytes apart — `<= W`
479
- * would wrongly merge them into one and break that pinned requirement. */
480
- function countClusters(spans, W) {
481
- if (spans.length === 0)
482
- return 0;
483
- const sorted = [...spans].sort((a, b) => a[0] - b[0]);
484
- let clusters = 1;
485
- let curEnd = sorted[0][1];
486
- for (let i = 1; i < sorted.length; i++) {
487
- const [s, e] = sorted[i];
488
- if (s - curEnd < W) {
489
- curEnd = Math.max(curEnd, e);
490
- }
491
- else {
492
- clusters++;
493
- curEnd = e;
494
- }
495
- }
496
- return clusters;
497
- }
498
- export function commitVotes(ctx, pooled, sat, regions, regionVoter, N) {
499
- const { votes, votesIdf, support, regionSupport, regionSpans, steps } = pooled;
500
- if (votes.size === 0) {
501
- traceAttention(ctx, regions, regionVoter, [], steps);
502
- return { roots: [], ranked: [] };
503
- }
504
- // SCALE-INVARIANT confidence — see Attention.breadth's doc. regions.length
505
- // is the query's OWN full candidate count (most never vote at all), the
506
- // same denominator the "N of M sub-regions voted" rationale text already
507
- // reports; regionSupport is that same accounting read PER ANCHOR.
508
- const totalRegions = Math.max(1, regions.length);
509
- const ranked = [...votes.entries()]
510
- .map(([anchor, vote]) => {
511
- const s = support.get(anchor);
512
- return {
513
- anchor,
514
- vote,
515
- start: s.start,
516
- end: s.end,
517
- breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
518
- clusters: countClusters(regionSpans.get(anchor) ?? [], ctx.space.maxGroup),
519
- };
520
- })
521
- .sort((a, b) => b.vote - a.vote);
522
- const overlaps = (a, b) => a.start < b.end && b.start < a.end;
523
- const idfDesc = [...votesIdf.values()].sort((a, b) => b - a);
524
- const rootCut = naturalBreak(idfDesc);
525
- // A FURTHER point of attention (beyond the dominant one, which always
526
- // grounds) must clear the same absolute significance floor
527
- // recallByResonance trusts a climb anchor with — log(N) + 1/2, three-ish
528
- // halvings of confidence above pure chance at this corpus scale — not
529
- // merely beat whatever its immediate neighbour in the ratio happens to be.
530
- // Without it, naturalBreak's ratio is scale-free but not FLOOR-free: on a
531
- // large, topic-diverse corpus the steepest ratio in a long noise tail can
532
- // sit far below any real signal, admitting scaffolding echoes as if they
533
- // were genuine further topics.
534
- const floor = consensusFloor(N);
535
- const placed = [];
536
- const roots = [];
537
- for (const point of ranked) {
538
- const absorbed = placed.some((p) => overlaps(point, p));
539
- if (!absorbed) {
540
- const pastLeading = !sat.hasLeading ||
541
- roots.length === 0 || point.start >= sat.leadingEnd;
542
- const vote = votesIdf.get(point.anchor) ?? 0;
543
- if ((roots.length === 0 || (vote >= rootCut && vote >= floor)) &&
544
- pastLeading) {
545
- roots.push(point);
546
- }
547
- else {
548
- continue;
549
- }
550
- }
551
- placed.push(point);
552
- }
553
- traceAttention(ctx, regions, regionVoter, roots, steps);
554
- return { roots, ranked };
555
- }
556
- export function detectSaturated(ctx, regions, saturated) {
557
- // Intervals are built from CHUNK regions only. collectRegions emits the
558
- // tree in POST-ORDER — a parent region arrives AFTER its children and
559
- // shares its first child's `start` — so the raw array is not monotone in
560
- // byte position, and a saturated parent would fuse with a later saturated
561
- // chunk into an interval swallowing a NON-saturated child. Chunk regions
562
- // (leaf-parents) are disjoint and already in byte order, and saturation
563
- // masking exists to drop canonicalFailed CHUNK votes (see poolVotes), so
564
- // chunks are both the sufficient and the safe basis. A region without a
565
- // `chunk` flag (a bare {start,end} from a direct caller) is treated as a
566
- // chunk.
567
- const intervals = [];
568
- let intStart = -1;
569
- let intEnd = -1;
570
- let totalLen = 0;
571
- for (let ri = 0; ri < regions.length; ri++) {
572
- const r = regions[ri];
573
- totalLen = Math.max(totalLen, r.end);
574
- if (r.chunk === false)
575
- continue;
576
- if (saturated[ri]) {
577
- if (intStart === -1)
578
- intStart = r.start;
579
- intEnd = r.end;
580
- }
581
- else {
582
- if (intStart !== -1) {
583
- intervals.push({ start: intStart, end: intEnd });
584
- intStart = -1;
585
- }
586
- }
587
- }
588
- if (intStart !== -1) {
589
- intervals.push({ start: intStart, end: intEnd });
590
- }
591
- const leading = intervals.length > 0 && intervals[0].start === 0
592
- ? intervals[0]
593
- : null;
594
- const hasLeading = leading !== null &&
595
- leading.end >= ctx.space.maxGroup &&
596
- leading.end < totalLen;
597
- const leadingEnd = leading !== null ? leading.end : 0;
598
- return { leadingEnd, hasLeading, intervals };
599
- }
600
- /** Set equality of two climb root lists (the "same conclusion" test the
601
- * contrastive margin skips rivals by). */
602
- function sameRoots(a, b) {
603
- if (a.length !== b.length)
604
- return false;
605
- const s = new Set(a);
606
- for (const x of b)
607
- if (!s.has(x))
608
- return false;
609
- return true;
610
- }
611
- export function canonicalChunkId(ctx, regionBytes, N, reachMemo) {
612
- const len = Math.min(regionBytes.length, ctx.space.maxGroup);
613
- for (let off = 0; off + len <= regionBytes.length; off++) {
614
- const ids = leafIdRun(ctx, regionBytes, off, off + len);
615
- if (ids === null)
616
- return null;
617
- const flatId = ctx.store.findBranch(ids);
618
- if (flatId === null)
619
- continue;
620
- if (len < 2)
621
- return flatId;
622
- let bestId = flatId;
623
- let bestReach = edgeAncestors(ctx, flatId, N, reachMemo);
624
- for (let k2 = 1; k2 < len; k2++) {
625
- const shortIds = ids.slice(0, len - k2);
626
- const shortId = ctx.store.findBranch(shortIds);
627
- if (shortId === null)
628
- continue;
629
- const shortReach = edgeAncestors(ctx, shortId, N, reachMemo);
630
- if (shortReach.saturated ||
631
- shortReach.contextsReached > bestReach.contextsReached) {
632
- bestId = shortId;
633
- bestReach = shortReach;
634
- }
635
- }
636
- return bestId;
637
- }
638
- return null;
639
- }
640
- export function naturalBreak(votes) {
641
- if (votes.length <= 1)
642
- return votes[0] ?? 0;
643
- let breakAt = 1;
644
- let steepest = Infinity;
645
- for (let i = 1; i < votes.length; i++) {
646
- if (votes[i - 1] <= 0)
647
- break;
648
- const ratio = votes[i] / votes[i - 1];
649
- if (ratio < steepest) {
650
- steepest = ratio;
651
- breakAt = i;
652
- }
653
- }
654
- return votes[breakAt - 1];
655
- }
656
- // ═══════════════════════════════════════════════════════════════════════════
657
- // Cross-region attention — DIRECT region-to-region interaction.
658
- //
659
- // voteRegions climbs each region INDEPENDENTLY; poolVotes then ADDS those
660
- // independent votes. Additive pooling is a soft conjunction, but it can only
661
- // ever surface a context at least one region already votes for. Two regions
662
- // whose individual climbs land on DIFFERENT contexts leave their JOINT context
663
- // — the learnt whole that contains BOTH — with no vote at all, and no amount
664
- // of pooling can recover it. ("red" climbs to `red square`, "circle" to
665
- // `circle`; nothing votes for `red circle`, the only fact holding both.)
666
- //
667
- // This is the attention counterpart of the bridge, and it ascends by the SAME
668
- // content-addressed junction walk (junction.ts): "which learnt whole contains
669
- // region A then region B?" is a bounded DAG ascent from the two forms'
670
- // canonical identities — NOT a resonance guess on a synthesised gist. Folding
671
- // two region vectors cannot even reconstruct the stored joint form: Sema builds
672
- // a multi-word gist from BYTE-chunk folds, so isolated word vectors superpose
673
- // into a different direction and resonate to `red circle` and `red square`
674
- // indistinguishably. The ascent sidesteps this by matching BYTES, not vectors.
675
- //
676
- // A joint container is EXACT evidence (it literally holds both forms), so it
677
- // votes at full strength — the exact-first discipline voteRegions gives a
678
- // content-addressed chunk. Each junction search is a bounded walk with NO
679
- // ANN query, and searches are capped at k. Three further disciplines make
680
- // the composition ORDER-FREE, N-ARY, and CORPUS-INDEPENDENT:
681
- //
682
- // • ORDER-FREE — a junction is evidence the forms were LEARNT TOGETHER;
683
- // which one the query mentioned first is a fact about the query, not the
684
- // learnt whole. The walk tests both byte orders at no extra walk cost
685
- // (see junctionContainersFrom's `unordered`).
686
- // • N-ARY — binding is not intrinsically pairwise. A pair's containers are
687
- // FILTERED by the remaining candidate forms: the container covering the
688
- // MOST of the query's composable forms wins, so three cross-cutting
689
- // attributes (each pair ambiguous) still resolve to their unique triple —
690
- // at the cost of one cached byte read + indexOf per (container, extra),
691
- // never an extra walk.
692
- // • CORPUS-INDEPENDENT — candidates are ANY voted region, not just
693
- // recognised sites. A word never learnt standalone has no site, but its
694
- // stored chunks still vote and their BYTES still compose: the ascent
695
- // matches byte containment, so a fragment pair evidences the same joint
696
- // container the whole word would. Contiguous shards of one word cannot
697
- // pair (the adjacency skip), and a pair covered by a single KNOWN region
698
- // is skipped — that whole form already votes directly, and re-deriving it
699
- // from its own pieces would only double-count.
700
- //
701
- // EXPLAINING AWAY (the aliasing complement of corpus independence): a chunk
702
- // of the query can straddle the byte grid so that it exists verbatim in the
703
- // WRONG deposit (" cir" of "red then circle" is a stored chunk of `blue
704
- // circle`, never of `red circle` — a pure alignment accident) and its
705
- // independent climb then votes for a context the query gives no reason to
706
- // believe. When a junction binds, any individual vote whose bytes the joint
707
- // container LITERALLY CONTAINS yet whose climb disagrees with the junction's
708
- // is superseded: the exact joint evidence explains those bytes, so their
709
- // disagreeing vote is grid aliasing, not signal. Votes whose bytes the
710
- // container does not hold (a genuine second topic) are untouched.
711
- // ═══════════════════════════════════════════════════════════════════════════
712
- async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
713
- // Candidate regions: every region that ALREADY CAST ITS OWN VOTE in
714
- // voteRegions — individually idf > 0, genuinely discriminative on its own,
715
- // just not necessarily for the SAME context as its partner. This is the
716
- // exact shape of the binding problem: "red" alone votes for `red square`,
717
- // "circle" alone for `circle` — each independently informative, disagreeing
718
- // on the conclusion — and only their CONJUNCTION resolves to the one
719
- // context, `red circle`, that actually holds both.
720
- //
721
- // A region that never voted (idf == 0 — e.g. a repeated system-prompt
722
- // prefix shared by every deposit) carries NO individual signal, and must be
723
- // excluded here too: ascending from a non-discriminative fragment's seeds
724
- // can still land on some deeper, incidentally-unique DESCENDANT container —
725
- // its rarity would come entirely from context OUTSIDE the fragments
726
- // actually composed, manufacturing confidence the query gave no reason to
727
- // have. Requiring a prior individual vote is the same discipline the noise
728
- // drop already applies to single regions, extended to compositions — with
729
- // one graded relaxation: a KNOWN region that did NOT vote (saturated, or
730
- // idf ≤ 0) may still serve as the WEAK side of a pair whose other side DID
731
- // vote. Saturation is an abstention about where the region CLIMBS; its
732
- // content-addressed identity is still exact, and the junction asks a
733
- // different question — "which whole holds both?" — whose conclusion the
734
- // container's own idf gate below still guards. Two non-voting regions
735
- // never pair (that is exactly the shared-prefix trap above), so at least
736
- // one side is always individually discriminative.
737
- //
738
- // Only MAXIMAL spans compose: a span contained in another candidate is a
739
- // fragment of that candidate's evidence, never independent of it.
740
- const votedSpans = new Set();
741
- for (const rv of rvs.votes)
742
- votedSpans.add(`${rv.start},${rv.end}`);
743
- const seen = new Set();
744
- const eligible = [];
745
- const strong = new Set();
746
- for (let ri = 0; ri < regions.length; ri++) {
747
- const r = regions[ri];
748
- const key = `${r.start},${r.end}`;
749
- const isStrong = votedSpans.has(key);
750
- if ((!isStrong && !r.known) || seen.has(key))
751
- continue;
752
- seen.add(key);
753
- eligible.push(ri);
754
- if (isStrong)
755
- strong.add(ri);
756
- }
757
- const cand = eligible.filter((x) => !eligible.some((y) => y !== x &&
758
- regions[y].start <= regions[x].start &&
759
- regions[x].end <= regions[y].end &&
760
- regions[y].end - regions[y].start > regions[x].end - regions[x].start));
761
- const none = { votes: [], superseded: new Set() };
762
- if (cand.length < 2)
763
- return none;
764
- cand.sort((x, y) => regions[x].start - regions[y].start || regions[x].end - regions[y].end);
765
- const dec = (b) => new TextDecoder().decode(b).replace(/\s+/g, " ").trim();
766
- const cache = walkCache(ctx);
767
- // One junctionSeeds per candidate for the WHOLE pairing loop — a candidate
768
- // recurs in up to |cand|−1 pairs, and its seeds are a pure function of its
769
- // bytes.
770
- const seedsMemo = new Map();
771
- const seedsOf = (ri) => {
772
- let s = seedsMemo.get(ri);
773
- if (s === undefined) {
774
- const r = regions[ri];
775
- s = junctionSeeds(ctx, query.subarray(r.start, r.end));
776
- seedsMemo.set(ri, s);
777
- }
778
- return s;
779
- };
780
- const overlapsSpan = (e, s) => e.start < s.end && s.start < e.end;
781
- const out = [];
782
- const superseded = new Set();
783
- // A candidate consumed by one junction does not seed another: its evidence
784
- // is already composed at full joint strength, and re-pairing it would vote
785
- // the same container (or a sub-container of it) twice.
786
- const consumed = new Set();
787
- let probes = 0;
788
- for (let a = 0; a < cand.length && probes < k; a++) {
789
- if (consumed.has(cand[a]))
790
- continue;
791
- const ra = regions[cand[a]];
792
- for (let b = a + 1; b < cand.length && probes < k; b++) {
793
- if (consumed.has(cand[b]))
794
- continue;
795
- const rb = regions[cand[b]];
796
- if (!strong.has(cand[a]) && !strong.has(cand[b]))
797
- continue;
798
- if (ra.end >= rb.start)
799
- continue; // overlap or adjacent — nothing between
800
- // Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
801
- // that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
802
- // genuine, individually-corroborated evidence about what fills the gap
803
- // — gate the container search below: a joint container is binding
804
- // evidence only when it is CONSISTENT with that evidence, i.e. its own
805
- // bytes actually contain what the between-region says. This is the
806
- // n-ary composition's normal shape (a between-attribute's bytes DO
807
- // recur inside the joint container, credited as an "extra" below) as
808
- // opposed to a container that silently substitutes something else for
809
- // it (e.g. bridging past "Italy" to a container whose interior is
810
- // "Japan" — a different, contradicting learnt whole).
811
- // Only a KNOWN (content-addressed, exact) between-region qualifies —
812
- // an approximate region's resonance climbing "somewhere" is ordinary
813
- // noise (any ANN query returns SOME nearest neighbour), not evidence
814
- // this specific gap already means something specific.
815
- const between = [];
816
- for (let m = a + 1; m < b; m++) {
817
- if (strong.has(cand[m]) && !consumed.has(cand[m]) &&
818
- regions[cand[m]].known)
819
- between.push(cand[m]);
820
- }
821
- // A single KNOWN region covering both: the whole form is already a
822
- // stored identity that votes directly; its pieces add nothing.
823
- if (regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end))
824
- continue;
825
- probes++;
826
- const left = query.subarray(ra.start, ra.end);
827
- const right = query.subarray(rb.start, rb.end);
828
- // Phrase-scale contract, exactly as the bridge: the glue between the two
829
- // forms may be up to W× the content it joins.
830
- const maxInterior = (left.length + right.length) * ctx.space.maxGroup;
831
- const cap = left.length + right.length + maxInterior;
832
- // Tier 1 — exact containers (both forms as substrings, either order, by
833
- // DAG ascent). Exact evidence first; only falls through to synonyms
834
- // when the exact ascent finds nothing — the SAME graded ladder the
835
- // bridge uses.
836
- let containers = junctionContainersFrom(ctx, left, right, cap, seedsOf(cand[a]), seedsOf(cand[b]), undefined, true);
837
- if (containers.length === 0) {
838
- // Tier 2.5 — synonym containers (halo sibling of one side + the other).
839
- containers = await junctionSynonyms(ctx, left, right, maxInterior, true);
840
- }
841
- if (containers.length === 0)
842
- continue;
843
- // N-ARY selection: the container covering the MOST remaining candidate
844
- // forms wins (then tightest interior, then lowest id). Reads are
845
- // cache hits — every container's bytes were already read by the walk.
846
- //
847
- // SELF-EVIDENCE GUARD: a junction is BINDING evidence only when the
848
- // container joins forms the query mentions APART. When the container's
849
- // own joined occurrence (left..right including its interior) is a
850
- // literal substring of the query, the query already spells that phrase
851
- // out contiguously — perception already voted with it, and grid shards
852
- // of one phrase pairing "around" a gap chunk would merely rediscover
853
- // the phrase they are shards of, then explain away its rivals.
854
- let best = null;
855
- let bestExtras = [];
856
- let bestCov = -1;
857
- for (const c of containers) {
858
- const bytes = cachedRead(ctx, cache, c.id, cap);
859
- const li = indexOf(bytes, left, 0);
860
- const ri = indexOf(bytes, right, 0);
861
- if (li >= 0 && ri >= 0) {
862
- const joined = bytes.subarray(Math.min(li, ri), Math.max(li + left.length, ri + right.length));
863
- if (indexOf(query, joined, 0) >= 0)
864
- continue; // query says it itself
865
- }
866
- // CONTRADICTION GUARD: a between-region already carrying its own
867
- // vote must actually recur in this container's bytes — otherwise
868
- // the container is a different learnt whole that happens to share
869
- // ra/rb, and letting it stand in for the gap would silently
870
- // override evidence the query itself already resolved there.
871
- if (between.some((bi) => indexOf(bytes, query.subarray(regions[bi].start, regions[bi].end), 0) < 0)) {
872
- continue;
873
- }
874
- let cov = left.length + right.length;
875
- const extras = [];
876
- for (const ei of cand) {
877
- if (ei === cand[a] || ei === cand[b] || consumed.has(ei))
878
- continue;
879
- const e = regions[ei];
880
- if (overlapsSpan(e, ra) || overlapsSpan(e, rb))
881
- continue;
882
- const eb = query.subarray(e.start, e.end);
883
- if (indexOf(bytes, eb, 0) >= 0) {
884
- extras.push(ei);
885
- cov += eb.length;
886
- }
887
- }
888
- if (cov > bestCov ||
889
- (cov === bestCov && best !== null &&
890
- (c.interior.length < best.interior.length ||
891
- (c.interior.length === best.interior.length && c.id < best.id)))) {
892
- best = c;
893
- bestExtras = extras;
894
- bestCov = cov;
895
- }
896
- }
897
- if (best === null)
898
- continue; // every container was self-evidence
899
- const reach = edgeAncestors(ctx, best.id, N, reachMemo);
900
- if (reach.saturated || reach.roots.length === 0)
901
- continue;
902
- const idf = Math.log(N / Math.max(1, reach.contextsReached));
903
- if (idf <= 0)
904
- continue;
905
- // EXACT joint evidence (score = 1): the container literally contains
906
- // every composed form. Mutual-explanation weight over their COMBINED
907
- // byte length — the same magnitude reading voteRegions uses, here with
908
- // the estimator collapsed to certainty, so mutual = min(ratio, 1/ratio).
909
- const lenR = Math.max(1, bestCov);
910
- const ratio = Math.sqrt(Math.max(1, ctx.store.contentLen(best.id, lenR * ctx.store.D)) / lenR);
911
- const mutual = Math.min(1, ratio) * Math.min(1, 1 / ratio);
912
- const w = (mutual * idf) / reach.roots.length;
913
- let spanStart = ra.start;
914
- let spanEnd = rb.end;
915
- for (const ei of bestExtras) {
916
- spanStart = Math.min(spanStart, regions[ei].start);
917
- spanEnd = Math.max(spanEnd, regions[ei].end);
918
- }
919
- consumed.add(cand[a]);
920
- consumed.add(cand[b]);
921
- for (const ei of bestExtras)
922
- consumed.add(ei);
923
- // EXPLAINING AWAY — see the block comment above the function. Byte
924
- // containment in the joint container is the relatedness test (the
925
- // vote's bytes are literally part of the learnt whole), and FULL root
926
- // disjointness is the disagreement test: a vote sharing even one root
927
- // with the junction corroborates it and keeps its say elsewhere.
928
- // Counted BEFORE pushing the junction's own vote below: each ORIGINAL
929
- // region this ascent explains away is evidence the junction speaks
930
- // for, not evidence lost — `absorbed` (RegionVote's breadth-accounting
931
- // field) must credit the junction with all of it, not just the ONE
932
- // pooled axiom it collapses to.
933
- const containerBytes = cachedRead(ctx, cache, best.id, cap);
934
- const jointRoots = new Set(reach.roots);
935
- let explainedAway = 0;
936
- for (const rv of rvs.votes) {
937
- if (rv.roots.some((r) => jointRoots.has(r)))
938
- continue;
939
- const bytes = query.subarray(rv.start, rv.end);
940
- if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
941
- superseded.add(rv);
942
- explainedAway++;
943
- }
944
- }
945
- out.push({
946
- start: spanStart,
947
- end: spanEnd,
948
- canonicalFailed: false, // content-addressed: never saturation-masked
949
- roots: reach.roots,
950
- w,
951
- wFocus: w,
952
- absorbed: 1 + explainedAway,
953
- });
954
- const label = [cand[a], cand[b], ...bestExtras]
955
- .sort((x, y) => regions[x].start - regions[y].start)
956
- .map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
957
- .join(" ▸ ");
958
- ctx.trace?.step("crossRegion", [{ text: label, role: "pair" }], reach.roots.map((r) => ({
959
- text: dec(read(ctx, r)).slice(0, 60),
960
- node: r,
961
- role: "joint-context",
962
- })), `${label} → junction node ${best.id}` +
963
- (best.interior.length === 0
964
- ? " (adjacent)"
965
- : ` (interior "${dec(best.interior)}")`) +
966
- ` → ${reach.roots.length} context(s), by content-addressed ascent` +
967
- (superseded.size > 0
968
- ? `; ${superseded.size} aliasing vote(s) explained away`
969
- : ""));
970
- break; // ra is consumed — move to the next unconsumed candidate
971
- }
972
- }
973
- return { votes: out, superseded };
974
- }
975
- export function traceAttention(ctx, regions, regionVoter, roots, steps = []) {
976
- if (!ctx.trace)
977
- return;
978
- const voters = [];
979
- for (let i = 0; i < regions.length; i++) {
980
- const rv = regionVoter[i];
981
- if (rv == null)
982
- continue;
983
- const item = rNode(ctx, rv.id, "sub-region", rv.score);
984
- item.text = `${item.text} (df-w ${rv.w.toFixed(2)})`;
985
- voters.push(item);
986
- }
987
- const t = ctx.trace.enter("climbConsensus", voters);
988
- // The pooled-evidence decision, one DerivationStep per anchor — the same
989
- // shape {@link GraphSearch}'s own cover steps take (see traceDerivation).
990
- if (steps.length > 0)
991
- traceDerivation(ctx, steps);
992
- t.done(roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)), roots.length === 0
993
- ? `${regions.length} sub-regions climbed the DAG, but none agreed on a context`
994
- : roots.length === 1
995
- ? `${voters.length} of ${regions.length} sub-regions voted; IDF-weighted consensus picked one context (vote ${roots[0].vote.toFixed(2)})`
996
- : `${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(", ")})`);
997
- }