@hviana/sema 0.2.2 → 0.2.4

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/mind/articulation.js +1 -1
  2. package/dist/src/mind/attention.d.ts +18 -2
  3. package/dist/src/mind/attention.js +88 -18
  4. package/dist/src/mind/bridge.d.ts +30 -0
  5. package/dist/src/mind/bridge.js +569 -0
  6. package/dist/src/mind/graph-search.d.ts +16 -1
  7. package/dist/src/mind/graph-search.js +34 -5
  8. package/dist/src/mind/match.d.ts +15 -2
  9. package/dist/src/mind/match.js +3 -8
  10. package/dist/src/mind/mechanisms/alu.js +8 -1
  11. package/dist/src/mind/mechanisms/cast.d.ts +54 -0
  12. package/dist/src/mind/mechanisms/cast.js +303 -48
  13. package/dist/src/mind/mechanisms/cover.js +24 -32
  14. package/dist/src/mind/mechanisms/extraction.js +75 -30
  15. package/dist/src/mind/mechanisms/recall.js +66 -0
  16. package/dist/src/mind/mind.d.ts +1 -0
  17. package/dist/src/mind/mind.js +6 -1
  18. package/dist/src/mind/pipeline.js +34 -2
  19. package/dist/src/mind/reasoning.d.ts +20 -1
  20. package/dist/src/mind/reasoning.js +84 -6
  21. package/dist/src/mind/recognition.js +157 -13
  22. package/dist/src/mind/traverse.js +16 -0
  23. package/dist/src/mind/types.d.ts +65 -2
  24. package/dist/src/mind/types.js +53 -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/mind/articulation.ts +1 -0
  29. package/src/mind/attention.ts +105 -17
  30. package/src/mind/bridge.ts +596 -0
  31. package/src/mind/graph-search.ts +59 -2
  32. package/src/mind/match.ts +19 -5
  33. package/src/mind/mechanisms/alu.ts +8 -1
  34. package/src/mind/mechanisms/cast.ts +336 -46
  35. package/src/mind/mechanisms/cover.ts +31 -36
  36. package/src/mind/mechanisms/extraction.ts +101 -40
  37. package/src/mind/mechanisms/recall.ts +79 -0
  38. package/src/mind/mind.ts +7 -1
  39. package/src/mind/pipeline.ts +37 -2
  40. package/src/mind/reasoning.ts +97 -5
  41. package/src/mind/recognition.ts +160 -12
  42. package/src/mind/traverse.ts +17 -0
  43. package/src/mind/types.ts +110 -6
  44. package/src/store.ts +19 -5
  45. package/test/35-attention-confidence.test.mjs +139 -0
  46. package/test/36-already-answered-fusion.test.mjs +128 -0
  47. package/test/37-cluster-dispersion-fusion.test.mjs +190 -0
  48. package/test/38-reason-restate-guard.test.mjs +94 -0
  49. package/test/39-cast-restate-guard.test.mjs +102 -0
  50. package/test/40-choosenext-scale-guard.test.mjs +75 -0
  51. package/test/41-seatofnode-direction.test.mjs +85 -0
  52. package/test/42-recognise-trace-idempotence.test.mjs +106 -0
  53. package/test/43-cast-analog-seat.test.mjs +244 -0
  54. package/test/44-recognise-edge-whitespace.test.mjs +63 -0
  55. package/test/45-liftanswer-restated-trim.test.mjs +60 -0
  56. package/test/46-recognise-multibyte-edge.test.mjs +85 -0
  57. package/test/47-cast-comparison-coverage.test.mjs +134 -0
  58. package/test/48-recognise-turn-connective.test.mjs +125 -0
  59. package/test/49-natural-units-synonym-bridge.test.mjs +175 -0
  60. package/test/50-cast-analog-consensus-floor.test.mjs +179 -0
@@ -70,43 +70,118 @@ export async function extractBySkill(
70
70
  if (ranked.length === 0) {
71
71
  return fail("no consensus anchor — no skill to apply");
72
72
  }
73
- let exemplar: SkillInfo | null = null;
74
- let chosenAnchor: number | null = null;
75
- let skipped = 0;
76
- for (const cand of ranked) {
77
- const ex = await pre.spanShapedOf(cand.anchor);
78
- if (ex) {
79
- exemplar = ex;
80
- chosenAnchor = cand.anchor;
81
- break;
73
+
74
+ // Try ranked anchors IN ORDER until one yields a USABLE extraction — not
75
+ // merely a span-shaped exemplar, but one whose extracted span clears the
76
+ // same one-river-fold quantum (W) cover.ts's restatedSpan gate already
77
+ // treats as the floor below which byte overlap is chance, not evidence.
78
+ // isSpanShaped (spanShapedOf) is a deliberately permissive sparse-
79
+ // subsequence check — see the section note below — so it accepts exemplars
80
+ // whose relation to the query is coincidental gap-matching, not genuine
81
+ // structure. Stopping at the FIRST such exemplar let a coincidental match
82
+ // early in the ranked list win outright and read out a sub-quantum
83
+ // fragment (observed: a 3-byte "Hel" pulled from an unrelated exemplar,
84
+ // while a later ranked anchor would have read the query's own "Hello…"
85
+ // correctly). Trying further anchors when one produces nothing usable is
86
+ // the same idiom this loop already uses for non-exemplars — extended to
87
+ // cover a bad extraction, not just a structural non-match.
88
+ //
89
+ // The retry is bounded at pre.k — the SAME evidence-breadth constant every
90
+ // other consumer of a ranked list already self-limits to (resonance, the
91
+ // weave, the climb itself; see Precomputed.k's own doc comment) — not the
92
+ // full ranked list. locate()'s frame match has an EXACT-byte tier with no
93
+ // significance correction of its own (short W-byte frames are cheap to
94
+ // match by pure chance), so trying every ranked anchor turns that per-
95
+ // anchor chance into a near-certainty over enough attempts: on a pure-
96
+ // gibberish query, 170 anchors deep found an unrelated Zulu exemplar whose
97
+ // short frame happened to byte-match, producing "xyzzy pl" — a coincidence
98
+ // no different in kind from the "RaBitQ estimate overshot the reach bar
99
+ // and grounded pure gibberish" failure recall.ts's own significance
100
+ // correction exists to prevent (see recallByResonance's reach-threshold
101
+ // comment). Bounding the search to the ranked list's own top-k restores
102
+ // the "genuinely relevant but not root-significant" exemplars this loop
103
+ // was built for, without the unbounded tail's chance collisions.
104
+ const W = ctx.space.maxGroup;
105
+ const searched = ranked.slice(0, pre.k);
106
+ let shapeMisses = 0;
107
+ let subQuantum = 0;
108
+ for (const cand of searched) {
109
+ const exemplar = await pre.spanShapedOf(cand.anchor);
110
+ if (!exemplar) {
111
+ shapeMisses++;
112
+ continue;
113
+ }
114
+ const built = buildFromExemplar(ctx, query, pre, exemplar);
115
+ if (built === null || built.bytes.length < W) {
116
+ subQuantum++;
117
+ continue;
118
+ }
119
+ if (shapeMisses > 0 || subQuantum > 0) {
120
+ ctx.trace?.step(
121
+ "trySkillAnchors",
122
+ [
123
+ rItem(
124
+ query.subarray(0, 0),
125
+ `skipped ${shapeMisses + subQuantum}`,
126
+ ),
127
+ rNode(ctx, cand.anchor, "chosen"),
128
+ ],
129
+ [],
130
+ `skipped ${shapeMisses} non-exemplar and ${subQuantum} sub-quantum ` +
131
+ `anchor(s) before one yielded a usable extraction`,
132
+ );
82
133
  }
83
- skipped++;
134
+ t?.done(
135
+ [rItem(built.bytes, "extracted")],
136
+ built.pieces === 1
137
+ ? `apply a learnt extraction skill — read the analogous span of the query` +
138
+ ` framed like "${
139
+ decodeText(exemplar.answerBytes)
140
+ }" sits in its exemplar`
141
+ : `apply a learnt MULTI-PIECE skill — read ${built.pieces} analogous` +
142
+ ` pieces of the query and synthesize them like "${
143
+ decodeText(exemplar.answerBytes)
144
+ }"`,
145
+ );
146
+ return {
147
+ bytes: built.bytes,
148
+ accounted: built.accounted,
149
+ unexplained: unexplainedLabel(query, built.accounted),
150
+ };
84
151
  }
85
- if (exemplar === null) {
152
+ if (shapeMisses === searched.length) {
86
153
  ctx.trace?.step(
87
154
  "trySkillAnchors",
88
155
  [],
89
156
  [],
90
- `none of ${ranked.length} ranked anchor(s) is a span-shaped skill exemplar`,
157
+ `none of the top ${searched.length} ranked anchor(s) (of ${ranked.length} total) ` +
158
+ `is a span-shaped skill exemplar`,
91
159
  );
92
160
  return fail("no consensus root is a span-shaped skill exemplar");
93
161
  }
94
- if (skipped > 0) {
95
- ctx.trace?.step(
96
- "trySkillAnchors",
97
- [
98
- rItem(query.subarray(0, 0), `skipped ${skipped}`),
99
- rNode(ctx, chosenAnchor!, "chosen"),
100
- ],
101
- [],
102
- `skipped ${skipped} anchor(s) that are not skill exemplars`,
103
- );
104
- }
162
+ return fail(
163
+ "no ranked anchor yielded an extraction at or above the quantum floor",
164
+ );
165
+ }
166
+
167
+ /** Build the extracted bytes for ONE already-accepted span-shaped exemplar —
168
+ * factored out of {@link extractBySkill} so its anchor loop can try
169
+ * successive ranked candidates instead of committing to the first
170
+ * structural match. Null when the exemplar's answer does not decompose
171
+ * against its context, or no piece's frame locates in the query. */
172
+ function buildFromExemplar(
173
+ ctx: MindContext,
174
+ query: Uint8Array,
175
+ pre: Precomputed,
176
+ exemplar: SkillInfo,
177
+ ):
178
+ | { bytes: Uint8Array; accounted: Array<[number, number]>; pieces: number }
179
+ | null {
105
180
  const { contextBytes, answerBytes } = exemplar;
106
181
 
107
182
  const ansCtxRuns = answerRunsInContext(ctx, contextBytes, answerBytes);
108
183
  if (ansCtxRuns === null || ansCtxRuns.length === 0) {
109
- return fail("answer is not a subsequence of the context");
184
+ return null;
110
185
  }
111
186
 
112
187
  if (ansCtxRuns.length > 1) {
@@ -200,25 +275,11 @@ export async function extractBySkill(
200
275
  if (preBounded && postBounded) accounted.push([start, end]);
201
276
  }
202
277
  if (pieces.length === 0) {
203
- return fail("no answer piece's frame located in the query");
278
+ return null;
204
279
  }
205
280
 
206
281
  const out = pieces.length === 1 ? pieces[0] : concatBytes(pieces);
207
- t?.done(
208
- [rItem(out, "extracted")],
209
- pieces.length === 1
210
- ? `apply a learnt extraction skill — read the analogous span of the query` +
211
- ` framed like "${decodeText(answerBytes)}" sits in its exemplar`
212
- : `apply a learnt MULTI-PIECE skill — read ${pieces.length} analogous` +
213
- ` pieces of the query and synthesize them like "${
214
- decodeText(answerBytes)
215
- }"`,
216
- );
217
- return {
218
- bytes: out,
219
- accounted,
220
- unexplained: unexplainedLabel(query, accounted),
221
- };
282
+ return { bytes: out, accounted, pieces: pieces.length };
222
283
  }
223
284
 
224
285
  // ── The two span-shape readings: OPEN acceptance vs. STRONG decomposition ──
@@ -20,6 +20,7 @@ import { CONCEPT, STEP } from "../graph-search.js";
20
20
  import { unexplainedLabel } from "../rationale.js";
21
21
  import type { PipelineMechanism, Precomputed } from "../pipeline-mechanism.js";
22
22
  import { rItem, rNode } from "../trace.js";
23
+ import { substitutionBridge } from "../bridge.js";
23
24
 
24
25
  /** A recall result. */
25
26
  export interface RecallResult {
@@ -293,6 +294,84 @@ export async function recallByResonance(
293
294
  }
294
295
  }
295
296
  }
297
+ // 3b. Corroborated-substitution bridge — refusal-path only (bridge.ts).
298
+ // Every gist-based tier has failed; before refusing, align the query
299
+ // byte-for-byte against the trained contexts its own stored windows
300
+ // anchor, accepting mismatches only as corpus-attested, concept-bar
301
+ // substitutions. A bridged context grounds exactly like any hit —
302
+ // projected through its learnt edges — under the same restated-fragment
303
+ // guard tiers 0b/2 apply. Costs nothing on any answering path.
304
+ {
305
+ // The resonance hits already ranked above are handed to the bridge as
306
+ // PROPOSED candidates alongside its own anchor climbs: on a corpus this
307
+ // size a W-byte window is far too common for the clamped climb to
308
+ // single out the right trained context, while the whole-query gist
309
+ // already ranked it nearest (observed live: "what is the capital of
310
+ // france" resonating straight to "What is the capital of France?" yet
311
+ // refusing on the reach bar). Approximate scores propose; the bridge's
312
+ // byte-exact alignment and attestation gates decide.
313
+ //
314
+ // The proposal breadth here is widened PAST `k` — first by requesting
315
+ // hubBound(ctx) candidates instead of `k` (recall's own tiers above
316
+ // stay at `k`; this re-resonates only on the refusal path, exactly
317
+ // where the bridge itself already runs), AND by asking the index to
318
+ // search EXHAUSTIVELY. Both matter: the IVF only ever probes
319
+ // ⌈√clusters⌉ of them (store.ts's efFor) REGARDLESS of k — widening k
320
+ // alone just returns more hits from the SAME already-probed clusters,
321
+ // never a hit whose vector lives in an unprobed one. Measured live:
322
+ // "What is the chemical symbol for water?" needs "What is the
323
+ // chemical formula for water?", scoring only 0.58 against the
324
+ // query's gist (a MIDDLE-of-string word swap perturbs the river-fold
325
+ // tree hash far more than a same-length TAIL swap like the "carbon"/
326
+ // "oxygen" neighbours that outrank it at 0.87+) — absent from the
327
+ // resonance list even at k=5000, present and byte-exact-verified the
328
+ // moment it's force-fed to the bridge directly. `exhaustive` is the
329
+ // natural, tuning-free ceiling (probe every cluster) for a call that
330
+ // is ALREADY refusal-path-only and must not miss a candidate hiding
331
+ // behind an unlucky structural distance.
332
+ const wide = k < hubBound(ctx)
333
+ ? await ctx.store.resonate(queryGist, hubBound(ctx), true)
334
+ : whole;
335
+ const bridged = await substitutionBridge(
336
+ ctx,
337
+ query,
338
+ wide.map((h) => h.id),
339
+ );
340
+ if (bridged !== null) {
341
+ const g = await project(ctx, bridged.id, queryGist);
342
+ // A projection contained in a substituted candidate-side span is the
343
+ // substitution RESTATED as if it were knowledge — the exact failure
344
+ // observed live: "Darwin was born in England." bridged to the
345
+ // Einstein fact through " England." → " Germany." and would have
346
+ // voiced "Germany", an answer the substitution itself manufactured.
347
+ // The same principle as the restated-fragment guards above, extended
348
+ // to the bridge's own substitutions.
349
+ const cBytes = ctx.store.bytes(bridged.id);
350
+ const manufactured = g !== null &&
351
+ bridged.subs.some((s) =>
352
+ indexOf(cBytes.subarray(s.cs, s.ce), g!, 0) >= 0
353
+ );
354
+ if (
355
+ g !== null && g.length > 0 && !restates(g) && !manufactured &&
356
+ !(g.length < query.length && indexOf(query, g, 0) >= 0)
357
+ ) {
358
+ return ground(
359
+ g,
360
+ `substitution bridge — a trained context accounts for the query ` +
361
+ `up to ${bridged.subs.length} corroborated substitution(s)`,
362
+ // Accounted NOTHING — the same epistemic humility as the echo
363
+ // tier below: a substitution-bridged grounding is a last resort
364
+ // that must lose to ANY mechanism that actually explained the
365
+ // query (observed: pricing the aligned spans here outweighed
366
+ // extraction's correct answer in the grounding decider), while
367
+ // still beating silence when everything else refused.
368
+ [],
369
+ CONCEPT * bridged.subs.length + STEP,
370
+ );
371
+ }
372
+ }
373
+ }
374
+
296
375
  // The refusal/echo decision. The echo returns a stored form's bytes AS
297
376
  // the answer — a near-identity claim about the query — and identity-grade
298
377
  // decisions are never made on an estimated score ("approximate scores may
package/src/mind/mind.ts CHANGED
@@ -266,9 +266,15 @@ export class Mind implements MindContext {
266
266
  sites: ReadonlyArray<Site>;
267
267
  leaves: ReadonlyArray<Leaf>;
268
268
  splits: ReadonlySet<number>;
269
+ starts: ReadonlySet<number>;
269
270
  } {
270
271
  const r = recognise(this, bytes);
271
- return { sites: r.sites, leaves: r.leaves, splits: r.splits };
272
+ return {
273
+ sites: r.sites,
274
+ leaves: r.leaves,
275
+ splits: r.splits,
276
+ starts: r.starts,
277
+ };
272
278
  }
273
279
 
274
280
  /** Disambiguate among multiple learnt continuations of the same context node.
@@ -288,8 +288,43 @@ export async function think(
288
288
  ? new Set<number>()
289
289
  : new Set(recognise(ctx, answer).sites.map((s) => s.payload));
290
290
  const reasoned = await reason(ctx, query, answer, preConsumed, pre);
291
- const fused = provenance === "recall" || provenance === "recall-echo"
292
- ? await fuseAttention(ctx, query, reasoned, pre)
291
+
292
+ // Fuse only when the query has a genuine REMAINDER no mechanism's
293
+ // structural evidence touched at all. `decided.accounted` alone
294
+ // undercounts this: it is a COST-LADDER quantity (cover.ts prices its
295
+ // masked/computed spans at near-zero and deliberately leaves them out of
296
+ // `accounted` so PASS-bridged bytes are still charged), not a coverage
297
+ // one — a query fully explained by one computed span plus bridged
298
+ // connectors can report `accounted: []` while nothing is actually left
299
+ // unexplained. The genuine remainder is what NEITHER the winning
300
+ // candidate's accounted spans NOR any recognised extension's computed
301
+ // span (`pre.computed` — every mechanism's parse() output, ALU included)
302
+ // ever touched. A remainder under one river-fold quantum (W, the same
303
+ // floor cover.ts's restatedSpan and the honesty-density bar above both
304
+ // use) is bridging punctuation/whitespace, never a second topic —
305
+ // observed: a single space between two fully-computed arithmetic spans
306
+ // ("2+2 3+3") registered as "unaccounted" and pulled in an unrelated
307
+ // corpus fact, corrupting "4 6" into "4 63".
308
+ const explained: Array<[number, number]> = [
309
+ ...decided.accounted,
310
+ ...pre.computed.map((u): [number, number] => [u.i, u.j]),
311
+ ];
312
+ const remainder = unaccounted(explained);
313
+ // Whether the winning candidate's entire recognised substance is
314
+ // COMPUTED — every accounted span exactly a pre.computed span, nothing
315
+ // from a genuinely recognised/climbed site. fuseAttention's lone-root
316
+ // shortcut assumes a single point of attention already IS primary's own
317
+ // source; that assumption is exactly backwards for a pure computation
318
+ // (an ALU result has no anchor of its own) — see fuseAttention's
319
+ // `unclimbed` parameter, gated there by Attention.breadth so a
320
+ // coincidental echo (which this flag alone cannot distinguish) is still
321
+ // rejected.
322
+ const unclimbed = decided.accounted.length > 0 &&
323
+ decided.accounted.every(([i, j]) =>
324
+ pre.computed.some((u) => u.i === i && u.j === j)
325
+ );
326
+ const fused = remainder >= ctx.space.maxGroup
327
+ ? await fuseAttention(ctx, query, reasoned, pre, unclimbed)
293
328
  : reasoned;
294
329
 
295
330
  done(
@@ -13,6 +13,20 @@ import { joinWithBridge, pivotInto } from "./resonance.js";
13
13
  import type { Precomputed } from "./pipeline-mechanism.js";
14
14
  import type { Rationale } from "./rationale.js";
15
15
 
16
+ /** Whether `bytes` is a proper byte-subspan of `query` — already present in
17
+ * the question, so voicing it back only restates part of what was asked,
18
+ * never answers it. The exact guard recallByResonance already applies to
19
+ * its OWN grounding candidates (tier 1's `restates`, tier 2's subspan
20
+ * check, tier 0b's argument-binding subspan check) — every mechanism that
21
+ * walks a LEARNT CONTINUATION EDGE past an already-vetted grounding
22
+ * (reason()'s own hops below, and CAST's `projectCounterfactual` seat
23
+ * substitution — see cast.ts) needs the same guard applied to what the
24
+ * walk turns up, since `follow()`/`chooseNext`/`pivotInto` know nothing of
25
+ * the query at all — only of what structurally continues what. */
26
+ export function restatesQuery(query: Uint8Array, bytes: Uint8Array): boolean {
27
+ return bytes.length < query.length && indexOf(query, bytes, 0) >= 0;
28
+ }
29
+
16
30
  /** Extend a grounded answer forward across facts (multi-hop reasoning).
17
31
  * Pivots on the longest unconsumed learnt context each answer contains,
18
32
  * then follows the pivot's continuation to the next fact. Repeats up
@@ -91,7 +105,8 @@ export async function reason(
91
105
  const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
92
106
  if (
93
107
  fwd !== null && !bytesEqual(fwd, cur) &&
94
- (fwdId === null || !consumed.has(fwdId))
108
+ (fwdId === null || !consumed.has(fwdId)) &&
109
+ !restatesQuery(query, fwd)
95
110
  ) {
96
111
  consumeAll(curId);
97
112
  t ??= ctx.trace?.enter("reason", [
@@ -115,7 +130,7 @@ export async function reason(
115
130
 
116
131
  const fc = await follow(ctx, pivot, qv);
117
132
  consumeAll(pivot);
118
- if (fc === null || bytesEqual(fc, cur)) break;
133
+ if (fc === null || bytesEqual(fc, cur) || restatesQuery(query, fc)) break;
119
134
  t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
120
135
  ctx.trace?.step(
121
136
  "pivotStep",
@@ -141,6 +156,14 @@ export async function fuseAttention(
141
156
  query: Uint8Array,
142
157
  primary: Uint8Array,
143
158
  pre: Precomputed,
159
+ /** True when `primary` never touched the consensus climb at all — e.g. a
160
+ * pure ALU computation, which has no anchor of its own. commitVotes
161
+ * ALWAYS admits the dominant root regardless of its vote (attention.ts:
162
+ * "roots.length === 0 || …") on the assumption a lone root already IS
163
+ * primary's own source; that assumption is exactly backwards when
164
+ * primary is unclimbed. Absent or false preserves the original
165
+ * behaviour exactly. */
166
+ unclimbed = false,
144
167
  ): Promise<Uint8Array> {
145
168
  // When the answer is structurally drawn from the query itself
146
169
  // (extraction), it already spans all the query's pieces — fusion
@@ -155,17 +178,86 @@ export async function fuseAttention(
155
178
  // query, same k, same DF mode) — read them from Precomputed instead of
156
179
  // re-climbing, so even a traced response pays for the climb once.
157
180
  const forest = (await pre.attention()).roots;
158
- if (forest.length <= 1) return primary;
181
+ // A LONE root is ordinarily primary's own source — nothing to fuse. But
182
+ // when primary is unclimbed, the lone root was never checked against
183
+ // anything: it is admitted by commitVotes unconditionally, so it may be
184
+ // genuine consensus (Attention.breadth dominates — most of the query's
185
+ // OWN regions corroborate it) or a coincidental echo (breadth does not
186
+ // dominate — see test/35-attention-confidence). breadth is the SCALE-
187
+ // INVARIANT read of exactly this question: the raw IDF vote cannot serve
188
+ // here, since it is an absolute ln(N)-scaled quantity (a genuine root on
189
+ // a large store can score BELOW its own floor while a coincidental echo
190
+ // on a small one scores comfortably above its own, smaller, floor).
191
+ const lonePromotes = unclimbed && forest.length === 1 &&
192
+ forest[0].breadth > 0.5;
193
+ if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) {
194
+ return primary;
195
+ }
159
196
 
160
197
  const pieces: Array<{ start: number; bytes: Uint8Array }> = [
161
198
  { start: forest[0].start, bytes: primary },
162
199
  ];
163
200
  const qv = pre.guide; // once, not per root
201
+ const rest = lonePromotes ? forest : forest.slice(1);
164
202
  const t = ctx.trace?.enter("fuseAttention", [
165
203
  rItem(primary, "primary"),
166
- ...forest.slice(1).map((r) => rNode(ctx, r.anchor, "point", r.vote)),
204
+ ...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
167
205
  ]);
168
- for (const root of forest.slice(1)) {
206
+ for (const root of rest) {
207
+ // DISPERSION: this root's contributing regions are confined to a
208
+ // single cluster (see Attention.clusters) — one local neighbourhood of
209
+ // the query, not several separate places. Raw region count already
210
+ // failed to discriminate a coincidental match from a genuine further
211
+ // topic (test/24 gap 3.1 vs test/35's echo); dispersion is a different
212
+ // question — not how MUCH evidence, but how many separate PLACES in the
213
+ // query corroborate it — and a coincidental match is structurally
214
+ // confined to one cluster no matter how strongly it resonates.
215
+ //
216
+ // EXCEPTION: crossRegionVotes' own joint conclusions (a query naming
217
+ // two attributes that were only ever learnt TOGETHER — test/34's own
218
+ // binding corpus) are inherently ONE fused context and are pooled from
219
+ // a single synthetic region, so they always read as one cluster even
220
+ // though they already, by construction, account for both original
221
+ // mentions. `breadth` (dominates — the same > half-the-query bar used
222
+ // everywhere else) still correctly recognises these: a genuine joint
223
+ // binding explains the MAJORITY of the query's regions on its own,
224
+ // which a coincidental echo never does (verified: test/35's echo tops
225
+ // out at 0.40). So a root is trusted when EITHER measure alone
226
+ // indicates real signal — excluded only when BOTH are weak. Cheap and
227
+ // synchronous — checked before the async already-answered walk below.
228
+ if (root.clusters < 2 && root.breadth <= 0.5) {
229
+ ctx.trace?.step(
230
+ "singleCluster",
231
+ [rNode(ctx, root.anchor, "point", root.vote)],
232
+ [],
233
+ "this point's evidence is confined to one local neighbourhood of the query — not trusted as an independent topic",
234
+ );
235
+ continue;
236
+ }
237
+ // ALREADY ANSWERED: this root's own learnt continuation — the same
238
+ // content-addressed walk reason()'s echo guard already trusts
239
+ // (`ctx.store.prevCount(qId) > 0`), here applied per-candidate instead
240
+ // of to the whole query — is VERBATIM present later in the query. A
241
+ // query that embeds both an exchange's ask and its own already-given
242
+ // reply (a conversation's turn plus its own prior answer, concatenated
243
+ // raw by addTurn — or any caller pasting a transcript into one
244
+ // respond() call; the check is Mind-bookkeeping-free, so it treats both
245
+ // identically) has already spoken this root's answer — fusing it in
246
+ // again would only restate it. Deliberately NOT a magnitude measure:
247
+ // it fires on exact content-addressed recurrence, not on how strongly
248
+ // the root resonates.
249
+ const cont = await follow(ctx, root.anchor, qv);
250
+ if (
251
+ cont !== null && cont.length > 0 && indexOf(query, cont, root.end) >= 0
252
+ ) {
253
+ ctx.trace?.step(
254
+ "alreadyAnswered",
255
+ [rNode(ctx, root.anchor, "point", root.vote)],
256
+ [rItem(cont, "continuation")],
257
+ "this point's own learnt continuation already appears later in the query — already answered, not fused",
258
+ );
259
+ continue;
260
+ }
169
261
  const g = await project(ctx, root.anchor, qv);
170
262
  if (g === null || g.length === 0) continue;
171
263
  if (pieces.some((p) => indexOf(p.bytes, g, 0) >= 0)) continue;