@hviana/sema 0.2.1 → 0.2.3

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 (44) hide show
  1. package/dist/src/geometry.d.ts +28 -0
  2. package/dist/src/geometry.js +35 -0
  3. package/dist/src/mind/articulation.js +1 -1
  4. package/dist/src/mind/attention.d.ts +4 -0
  5. package/dist/src/mind/attention.js +65 -12
  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/learning.d.ts +1 -1
  9. package/dist/src/mind/learning.js +20 -5
  10. package/dist/src/mind/mechanisms/alu.js +8 -1
  11. package/dist/src/mind/mechanisms/cast.js +35 -11
  12. package/dist/src/mind/mechanisms/cover.js +40 -1
  13. package/dist/src/mind/mechanisms/extraction.js +75 -30
  14. package/dist/src/mind/mechanisms/recall.js +26 -2
  15. package/dist/src/mind/mind.d.ts +3 -2
  16. package/dist/src/mind/mind.js +28 -54
  17. package/dist/src/mind/pipeline.js +34 -2
  18. package/dist/src/mind/primitives.d.ts +16 -9
  19. package/dist/src/mind/primitives.js +58 -22
  20. package/dist/src/mind/reasoning.d.ts +9 -1
  21. package/dist/src/mind/reasoning.js +26 -4
  22. package/dist/src/mind/recognition.js +30 -6
  23. package/dist/src/mind/traverse.js +15 -0
  24. package/dist/src/mind/types.d.ts +58 -10
  25. package/dist/src/mind/types.js +15 -0
  26. package/package.json +1 -1
  27. package/src/geometry.ts +61 -1
  28. package/src/mind/articulation.ts +1 -0
  29. package/src/mind/attention.ts +80 -12
  30. package/src/mind/graph-search.ts +59 -2
  31. package/src/mind/learning.ts +20 -3
  32. package/src/mind/mechanisms/alu.ts +8 -1
  33. package/src/mind/mechanisms/cast.ts +46 -8
  34. package/src/mind/mechanisms/cover.ts +46 -0
  35. package/src/mind/mechanisms/extraction.ts +101 -40
  36. package/src/mind/mechanisms/recall.ts +30 -2
  37. package/src/mind/mind.ts +38 -61
  38. package/src/mind/pipeline.ts +37 -2
  39. package/src/mind/primitives.ts +71 -29
  40. package/src/mind/reasoning.ts +26 -3
  41. package/src/mind/recognition.ts +28 -5
  42. package/src/mind/traverse.ts +18 -0
  43. package/src/mind/types.ts +73 -10
  44. package/test/35-attention-confidence.test.mjs +139 -0
package/src/mind/mind.ts CHANGED
@@ -14,8 +14,6 @@ import { bindSeat, fold, Sema, Space } from "../sema.js";
14
14
  import { Alphabet } from "../alphabet.js";
15
15
  import {
16
16
  bytesToTree,
17
- bytesToTreePyramid,
18
- type FoldPyramid,
19
17
  Grid,
20
18
  gridToTree,
21
19
  hilbertBytes,
@@ -109,7 +107,7 @@ export interface Conversation {
109
107
  * foldTree returns their ids immediately — O(suffix) instead of
110
108
  * O(context) for every tree walk. */
111
109
  interface ConversationData {
112
- pyramid: FoldPyramid;
110
+ tree: Sema;
113
111
  bytes: Uint8Array;
114
112
  boundaries: number[];
115
113
  perceiveMemo: Map<string, Sema>;
@@ -244,9 +242,10 @@ export class Mind implements MindContext {
244
242
  // bounded: a pyramid costs ~KB per content byte (one D-float gist per
245
243
  // interior node), and only the few live conversation chains need to stay
246
244
  // warm, so 8 entries is the honest budget.
247
- _depositTrees = new BoundedMap<string, import("../geometry.js").FoldPyramid>(
248
- 8,
249
- );
245
+ _depositTrees = new BoundedMap<
246
+ string,
247
+ import("./types.js").DepositCacheEntry
248
+ >(8);
250
249
  _depositLens = new Set<number>();
251
250
  _internIds = new WeakMap<import("../sema.js").Sema, number>();
252
251
 
@@ -267,9 +266,15 @@ export class Mind implements MindContext {
267
266
  sites: ReadonlyArray<Site>;
268
267
  leaves: ReadonlyArray<Leaf>;
269
268
  splits: ReadonlySet<number>;
269
+ starts: ReadonlySet<number>;
270
270
  } {
271
271
  const r = recognise(this, bytes);
272
- 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
+ };
273
278
  }
274
279
 
275
280
  /** Disambiguate among multiple learnt continuations of the same context node.
@@ -513,18 +518,20 @@ export class Mind implements MindContext {
513
518
  * where one turn ends and the next begins. */
514
519
  beginConversation(state?: ConversationState): Conversation {
515
520
  const id = this._nextConvId++;
516
- // Build the initial pyramid: from scratch for a new conversation,
517
- // or from the saved context bytes when restoring.
518
521
  const initBytes = state?.context ?? new Uint8Array(0);
519
- const { pyramid } = bytesToTreePyramid(
522
+ const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
523
+ const tree = bytesToTree(
520
524
  this.space,
521
525
  this.alphabet,
522
526
  initBytes,
527
+ undefined,
528
+ undefined,
529
+ initBoundaries.length > 0 ? initBoundaries : undefined,
523
530
  );
524
531
  this._conversations.set(id, {
525
- pyramid,
532
+ tree,
526
533
  bytes: initBytes,
527
- boundaries: state?.boundaries ? [...state.boundaries] : [],
534
+ boundaries: initBoundaries,
528
535
  perceiveMemo: new Map(),
529
536
  recogniseMemo: new Map(),
530
537
  climbMemo: new Map(),
@@ -572,25 +579,23 @@ export class Mind implements MindContext {
572
579
  * place a context grows ({@link addTurn} and {@link respondTurn} both
573
580
  * come through here), so the append semantics cannot drift. */
574
581
  private _growContext(data: ConversationData, turnBytes: Uint8Array): Sema {
575
- const prevLen = data.pyramid.bytes;
582
+ const prevLen = data.bytes.length;
576
583
  // An empty turn neither grows the context nor marks a boundary —
577
584
  // boundaries are documented strictly increasing, and a zero-length
578
- // "turn" is no turn. The pyramid's zero-growth shortcut makes the
579
- // refold below O(1), so the existing tree is returned unchanged.
585
+ // "turn" is no turn. Nothing changed, so the existing tree stands.
580
586
  const grow = turnBytes.length > 0;
581
- const grown = !grow
582
- ? data.bytes
583
- : prevLen > 0
584
- ? concat2(data.bytes, turnBytes)
585
- : turnBytes;
586
- if (grow && prevLen > 0) data.boundaries.push(prevLen);
587
- const { tree, pyramid } = bytesToTreePyramid(
587
+ if (!grow) return data.tree;
588
+ const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
589
+ if (prevLen > 0) data.boundaries.push(prevLen);
590
+ const tree = bytesToTree(
588
591
  this.space,
589
592
  this.alphabet,
590
593
  grown,
591
- data.pyramid,
594
+ undefined,
595
+ undefined,
596
+ data.boundaries.length > 0 ? data.boundaries : undefined,
592
597
  );
593
- data.pyramid = pyramid;
598
+ data.tree = tree;
594
599
  data.bytes = grown;
595
600
  data.perceiveMemo.set(latin1Key(grown), tree);
596
601
  return tree;
@@ -644,43 +649,15 @@ export class Mind implements MindContext {
644
649
  this.canonMemo = this.canon ? new Map() : null;
645
650
 
646
651
  try {
647
- // Seed the recognise memo with the STANDALONE recognition of the
648
- // NEW turn, offset into the accumulated context. In the full-context
649
- // tree, content-defined chunk boundaries shift when bytes are
650
- // concatenated — foldTree no longer visits the original turn's root
651
- // node, so the structural pass misses it. Perceiving the turn alone
652
- // preserves intact structure. We seed ONLY the suffix (the new turn);
653
- // prefix forms stay visible through the full-context recognition
654
- // (they serve as context, not as questions to re-answer).
655
- if (data.boundaries.length > 0) {
656
- const suffixStart = data.boundaries[data.boundaries.length - 1];
657
- if (suffixStart < newContext.length) {
658
- const suffixBytes = newContext.subarray(suffixStart);
659
- const suffixRec = recognise(this, suffixBytes);
660
- const fullRec = recognise(this, newContext);
661
- const seen = new Set(fullRec.sites.map((s) => `${s.start},${s.end}`));
662
- const mergedSites = [...fullRec.sites];
663
- for (const site of suffixRec.sites) {
664
- const absStart = suffixStart + site.start;
665
- const absEnd = suffixStart + site.end;
666
- const key = `${absStart},${absEnd}`;
667
- if (!seen.has(key)) {
668
- seen.add(key);
669
- mergedSites.push({
670
- start: absStart,
671
- end: absEnd,
672
- payload: site.payload,
673
- });
674
- }
675
- }
676
- data.recogniseMemo.set(latin1Key(newContext), {
677
- sites: mergedSites,
678
- leaves: fullRec.leaves,
679
- splits: fullRec.splits,
680
- });
681
- }
682
- }
683
-
652
+ // No recognise-memo pre-seeding here: that used to be necessary
653
+ // because the flat/positional fold lost visibility into an earlier
654
+ // turn's own structure once later bytes shifted its position
655
+ // (foldTree no longer visited the turn's root node). The
656
+ // STABLE-PREFIX fold (see {@link ConversationData}) makes every
657
+ // turn's subtree independent of what follows it by construction, so
658
+ // recognise() finds it correctly on its own, first-touch, exactly
659
+ // once per turn no workaround needed, and none pre-empting
660
+ // recognise()'s own memo with a partial result.
684
661
  const top = this.trace?.enter("respondTurn", [
685
662
  rItem(newContext, "query"),
686
663
  ]);
@@ -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(
@@ -8,16 +8,16 @@ import { Sema } from "../sema.js";
8
8
  import {
9
9
  bytesToTree,
10
10
  bytesToTreePyramid,
11
- type FoldPyramid,
12
11
  Grid,
13
12
  gridToTree,
14
13
  hilbertBytes,
14
+ stablePrefixFoldIncremental,
15
15
  stackGrids,
16
16
  } from "../geometry.js";
17
17
  import { canonHash } from "../canon.js";
18
18
  import { bytesEqual } from "../bytes.js";
19
19
  import { ALL } from "./types.js";
20
- import type { Input, MindContext } from "./types.js";
20
+ import type { DepositCacheEntry, Input, MindContext } from "./types.js";
21
21
 
22
22
  // ── Address: bytes → node ──────────────────────────────────────────────
23
23
 
@@ -93,34 +93,76 @@ export function perceive(
93
93
  return gridToTree(ctx.space, ctx.alphabet, input as Grid);
94
94
  }
95
95
 
96
- /** The DEPOSIT-shaped perceive: the PLAIN fold (bit-identical to inference
97
- * perception that structural train/inference agreement is load-bearing
98
- * for exact recall), computed INCREMENTALLY via the fold's level pyramid
99
- * ({@link bytesToTreePyramid}). An accumulated context (a conversation)
100
- * grows by suffixes: the previous context's pyramid is cached by CONTENT
101
- * (ctx._depositTrees), and this deposit refolds only the right edge of
102
- * each level O(turn) instead of O(context) per turn. Purely a cache:
103
- * the produced tree never depends on cache state. */
104
- export function perceiveDeposit(ctx: MindContext, bytes: Uint8Array): Sema {
105
- let prev: FoldPyramid | undefined;
106
- // Longest cached PROPER prefix first.
107
- const lens = [...ctx._depositLens]
108
- .filter((L) => L >= 2 && L < bytes.length)
109
- .sort((a, b) => b - a);
110
- for (const L of lens) {
111
- const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
112
- if (hit !== undefined) {
113
- prev = hit;
114
- break;
96
+ /** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
97
+ * (bit-identical to inference perception of a standalone query that
98
+ * structural train/inference agreement is load-bearing for exact recall),
99
+ * computed incrementally via the fold's level pyramid
100
+ * ({@link bytesToTreePyramid}). An input that EXTENDS a previously
101
+ * deposited one is a conversation context grown by one turn — the cached
102
+ * prefix length IS the turn boundary (derived from the deposit sequence
103
+ * itself, never from content conventions) and takes the STABLE-PREFIX
104
+ * fold over the accumulated boundaries, bit-identical to the boundary
105
+ * fold query-time conversation perception uses, so the trained context
106
+ * node and the query's context subtree are the SAME node. Segment folds
107
+ * reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
108
+ * instead of O(context) per turn. The fold state is purely a cache; the
109
+ * boundary accumulation is what an evicted chain loses (falling back to
110
+ * the plain fold, the pre-boundary shape — a warm replay restores it). */
111
+ export function perceiveDeposit(
112
+ ctx: MindContext,
113
+ bytes: Uint8Array,
114
+ conversational = false,
115
+ ): Sema {
116
+ let prev: DepositCacheEntry | undefined;
117
+ let prefixLen = 0;
118
+ // Cache consult (both boundary lookup and stable-prefix reuse) is scoped
119
+ // to conversational deposits only — a bare, unrelated fact whose bytes
120
+ // happen to extend an earlier deposit is NOT a conversation turn, and
121
+ // must keep the plain fold so it shares structure with ITS OWN prior
122
+ // deposits, not fragment against a coincidental byte-prefix.
123
+ if (conversational) {
124
+ // Longest cached PROPER prefix first.
125
+ const lens = [...ctx._depositLens]
126
+ .filter((L) => L >= 2 && L < bytes.length)
127
+ .sort((a, b) => b - a);
128
+ for (const L of lens) {
129
+ const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
130
+ // The suffix must bytes-equal the hit's OWN recorded continuation —
131
+ // proof this deposit is that turn's actual next turn, not a fact
132
+ // that coincidentally shares its byte prefix.
133
+ if (
134
+ hit !== undefined && hit.nextBytes !== undefined &&
135
+ bytesEqual(hit.nextBytes, bytes.subarray(L))
136
+ ) {
137
+ prev = hit;
138
+ prefixLen = L;
139
+ break;
140
+ }
115
141
  }
116
142
  }
117
- const { tree, pyramid } = bytesToTreePyramid(
118
- ctx.space,
119
- ctx.alphabet,
120
- bytes,
121
- prev,
122
- );
123
- if (bytes.length >= 2) {
143
+ let tree: Sema;
144
+ let entry: DepositCacheEntry;
145
+ if (prev !== undefined) {
146
+ const boundaries = [...prev.boundaries, prefixLen];
147
+ const folded = stablePrefixFoldIncremental(
148
+ ctx.space,
149
+ ctx.alphabet,
150
+ bytes,
151
+ boundaries,
152
+ prev.stable,
153
+ );
154
+ tree = folded.tree;
155
+ entry = { boundaries, stable: folded.fold };
156
+ } else {
157
+ const plain = bytesToTreePyramid(ctx.space, ctx.alphabet, bytes);
158
+ tree = plain.tree;
159
+ entry = { boundaries: [], pyramid: plain.pyramid };
160
+ }
161
+ // Only a conversational deposit writes the cache too — otherwise a bare
162
+ // fact's plain fold could later be misread as a conversation's turn-zero
163
+ // boundary by an unrelated conversational deposit that happens to extend
164
+ // its bytes.
165
+ if (conversational && bytes.length >= 2) {
124
166
  // The lengths set drifts as the map evicts; past the probe budget the
125
167
  // drift itself becomes the cost (each stale length is an O(len) key
126
168
  // build), so both reset together — losing only warm-up on live chains.
@@ -128,7 +170,7 @@ export function perceiveDeposit(ctx: MindContext, bytes: Uint8Array): Sema {
128
170
  ctx._depositLens.clear();
129
171
  ctx._depositTrees.clear();
130
172
  }
131
- ctx._depositTrees.set(latin1Key(bytes), pyramid);
173
+ ctx._depositTrees.set(latin1Key(bytes), entry);
132
174
  ctx._depositLens.add(bytes.length);
133
175
  }
134
176
  return tree;
@@ -141,6 +141,14 @@ export async function fuseAttention(
141
141
  query: Uint8Array,
142
142
  primary: Uint8Array,
143
143
  pre: Precomputed,
144
+ /** True when `primary` never touched the consensus climb at all — e.g. a
145
+ * pure ALU computation, which has no anchor of its own. commitVotes
146
+ * ALWAYS admits the dominant root regardless of its vote (attention.ts:
147
+ * "roots.length === 0 || …") on the assumption a lone root already IS
148
+ * primary's own source; that assumption is exactly backwards when
149
+ * primary is unclimbed. Absent or false preserves the original
150
+ * behaviour exactly. */
151
+ unclimbed = false,
144
152
  ): Promise<Uint8Array> {
145
153
  // When the answer is structurally drawn from the query itself
146
154
  // (extraction), it already spans all the query's pieces — fusion
@@ -155,17 +163,32 @@ export async function fuseAttention(
155
163
  // query, same k, same DF mode) — read them from Precomputed instead of
156
164
  // re-climbing, so even a traced response pays for the climb once.
157
165
  const forest = (await pre.attention()).roots;
158
- if (forest.length <= 1) return primary;
166
+ // A LONE root is ordinarily primary's own source — nothing to fuse. But
167
+ // when primary is unclimbed, the lone root was never checked against
168
+ // anything: it is admitted by commitVotes unconditionally, so it may be
169
+ // genuine consensus (Attention.breadth dominates — most of the query's
170
+ // OWN regions corroborate it) or a coincidental echo (breadth does not
171
+ // dominate — see test/35-attention-confidence). breadth is the SCALE-
172
+ // INVARIANT read of exactly this question: the raw IDF vote cannot serve
173
+ // here, since it is an absolute ln(N)-scaled quantity (a genuine root on
174
+ // a large store can score BELOW its own floor while a coincidental echo
175
+ // on a small one scores comfortably above its own, smaller, floor).
176
+ const lonePromotes = unclimbed && forest.length === 1 &&
177
+ forest[0].breadth > 0.5;
178
+ if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) {
179
+ return primary;
180
+ }
159
181
 
160
182
  const pieces: Array<{ start: number; bytes: Uint8Array }> = [
161
183
  { start: forest[0].start, bytes: primary },
162
184
  ];
163
185
  const qv = pre.guide; // once, not per root
186
+ const rest = lonePromotes ? forest : forest.slice(1);
164
187
  const t = ctx.trace?.enter("fuseAttention", [
165
188
  rItem(primary, "primary"),
166
- ...forest.slice(1).map((r) => rNode(ctx, r.anchor, "point", r.vote)),
189
+ ...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
167
190
  ]);
168
- for (const root of forest.slice(1)) {
191
+ for (const root of rest) {
169
192
  const g = await project(ctx, root.anchor, qv);
170
193
  if (g === null || g.length === 0) continue;
171
194
  if (pieces.some((p) => indexOf(p.bytes, g, 0) >= 0)) continue;
@@ -52,7 +52,8 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
52
52
  const sites: Site[] = [];
53
53
  const leaves: Leaf[] = [];
54
54
  const splits = new Set<number>();
55
- if (bytes.length === 0) return { sites, leaves, splits };
55
+ const starts = new Set<number>();
56
+ if (bytes.length === 0) return { sites, leaves, splits, starts };
56
57
 
57
58
  // Span-resolve memo for THIS call: the structural pass (sub-runs inside
58
59
  // leaf-parents) and the canonical pass (leaf-id chains) probe overlapping
@@ -90,7 +91,6 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
90
91
  };
91
92
 
92
93
  // ── structural: the query's own perceived tree ──────────────────────
93
- const starts = new Set<number>();
94
94
  starts.add(0);
95
95
  foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
96
96
  if (n.kids === null) {
@@ -115,7 +115,15 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
115
115
  leafOffsets.push(off);
116
116
  off += k.leaf?.length ?? 0;
117
117
  }
118
+ // Sub-spans starting at i > 0 begin INSIDE the chunk, at an offset the
119
+ // query's own fold did not itself choose as a boundary — the same
120
+ // opportunistic byte-atom-chain risk `tryChain`'s `boundary` gate
121
+ // guards below (see its comment). Only the chunk's own left edge
122
+ // (i === 0, already registered in `starts` above) carries the fold's
123
+ // evidence; interior sub-starts are exempt from the guard only while
124
+ // atoms themselves still discriminate at this corpus scale.
118
125
  for (let i = 0; i < n.kids.length; i++) {
126
+ if (i > 0 && atomsAreHubs) break;
119
127
  const subIds: number[] = [];
120
128
  for (let j = i; j < n.kids.length; j++) {
121
129
  const kj = n.kids[j];
@@ -158,9 +166,23 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
158
166
  }
159
167
  }
160
168
 
169
+ // A chain rebuilt from a NON-boundary offset (the query's own perceived
170
+ // cut, `starts`, never chose to segment here) is opportunistic: the same
171
+ // byte-atom coincidence the hub guard above already exists for, just
172
+ // spelled over 2+ leaves instead of 1. At small corpus scale that's fine
173
+ // — coincidence is rare and every chain is real evidence (see `atomIsHub`).
174
+ // Past the scale where atoms themselves stop discriminating, the same
175
+ // uniform-expectation argument bounds a CHAIN'S commonality too: it is at
176
+ // least as rare as its rarest atom, so a store where atoms are hubs makes
177
+ // interior chain reconstructions no more trustworthy than the atoms they
178
+ // are built from ("hi" resolving out of "W[hi]ch" is exactly this: two
179
+ // hub-scale atoms, chained at an offset nothing in the query's own fold
180
+ // selected). Chains that start ON a boundary carry the fold's own
181
+ // evidence instead and are exempt.
161
182
  const tryChain = (
162
183
  p: number,
163
184
  maxIds: number,
185
+ boundary: boolean,
164
186
  ): void => {
165
187
  const first = leafFrom(p);
166
188
  if (!first) return;
@@ -174,6 +196,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
174
196
  ids.push(nx.id);
175
197
  pos = nx.end;
176
198
  if (store.findBranch(ids) === null) continue;
199
+ if (!boundary && atomsAreHubs) continue;
177
200
  const id = resolveSpan(p, pos);
178
201
  if (id === null || id === prevId) continue;
179
202
  prevId = id;
@@ -183,10 +206,10 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
183
206
 
184
207
  for (let p = 0; p < bytes.length; p++) {
185
208
  if (starts.has(p)) {
186
- tryChain(p, chainReach(W)); // boundary start — full reach
209
+ tryChain(p, chainReach(W), true); // boundary start — full reach
187
210
  } else {
188
211
  const limit = chunkEnd[p] + W;
189
- tryChain(p, Math.min(limit - p, chainReach(W)));
212
+ tryChain(p, Math.min(limit - p, chainReach(W)), false);
190
213
  }
191
214
  }
192
215
 
@@ -211,7 +234,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
211
234
  ` (over ${leaves.length} perceived leaves)`,
212
235
  );
213
236
 
214
- return { sites, leaves, splits };
237
+ return { sites, leaves, splits, starts };
215
238
  }
216
239
 
217
240
  /** Segment bytes using the geometry's own groupings — leaf-parent
@@ -7,6 +7,7 @@
7
7
  // project) live in match.ts — the elementary match-and-project operation.
8
8
 
9
9
  import { cosine, Vec } from "../vec.js";
10
+ import { consensusFloor } from "../geometry.js";
10
11
  import type { AncestorReach, MindContext } from "./types.js";
11
12
  import { gistOf, read } from "./primitives.js";
12
13
 
@@ -527,6 +528,23 @@ export function chooseNext(
527
528
  }
528
529
  }
529
530
 
531
+ // A pick among GENUINELY competing continuations still needs to clear the
532
+ // same genuine-corroboration floor {@link consensusFloor} draws for every
533
+ // other consumer that turns distinct-context support into a vote (the
534
+ // climb's recallByResonance/commitVotes) — below it, "most corroborated"
535
+ // is only the least-thin echo among several, not real evidence. Gated to
536
+ // corpus scale large enough that this echo is even possible (the same
537
+ // scale {@link atomIsHub} already switches on for the identical reason:
538
+ // at small N every edge IS the evidence there is). A hub id has no
539
+ // meaningful "distinct context" reading at all (id < 0 atoms are excluded
540
+ // from this path already, since chooseNext only ever sees real edges).
541
+ const N = corpusN(ctx);
542
+ if (
543
+ capped.length > 1 && atomIsHub(ctx, N) && bestSupport < consensusFloor(N)
544
+ ) {
545
+ return undefined;
546
+ }
547
+
530
548
  // Trace is built lazily — the filter + map below only execute when a
531
549
  // trace listener is attached, so the common (no-trace) path pays only
532
550
  // for the prevCount calls in the loop above, never for extra rItemShort
package/src/mind/types.ts CHANGED
@@ -19,7 +19,26 @@ import type {
19
19
  Site,
20
20
  } from "./graph-search.js";
21
21
  import type { Rationale } from "./rationale.js";
22
- import type { FoldPyramid, Grid } from "../geometry.js";
22
+ import type { FoldPyramid, Grid, StableFold } from "../geometry.js";
23
+
24
+ /** One {@link MindContext._depositTrees} entry — see that field's doc. */
25
+ export interface DepositCacheEntry {
26
+ /** Turn boundaries accumulated over this content's deposit chain —
27
+ * strictly increasing proper offsets, each a previously-deposited
28
+ * whole-context length. Empty for a first-seen (single-turn) input. */
29
+ boundaries: number[];
30
+ /** Plain-fold pyramid (first-seen inputs only). */
31
+ pyramid?: FoldPyramid;
32
+ /** Stable-prefix segment folds (grown-context inputs only). */
33
+ stable?: StableFold;
34
+ /** The continuation bytes this ctxInput was paired with in ingestPair, if
35
+ * any — the ONLY thing that makes a later, longer ctxInput a genuine next
36
+ * TURN of the same conversation rather than an unrelated fact that
37
+ * happens to share this one's byte prefix (e.g. "2+2" vs. "2+2=5"). A
38
+ * later deposit only takes this entry as its stable-prefix `prev` when
39
+ * its own suffix bytes-equal this exactly. */
40
+ nextBytes?: Uint8Array;
41
+ }
23
42
  import { concatBytes } from "../bytes.js";
24
43
  import { dominates } from "../geometry.js";
25
44
 
@@ -45,6 +64,7 @@ export interface GraphSearchHost {
45
64
  sites: ReadonlyArray<Site>;
46
65
  leaves: ReadonlyArray<Leaf>;
47
66
  splits: ReadonlySet<number>;
67
+ starts: ReadonlySet<number>;
48
68
  };
49
69
  chooseNext?(node: number): number | undefined;
50
70
  }
@@ -60,6 +80,13 @@ export interface Recognition {
60
80
  leaves: Leaf[];
61
81
  /** Sub-leaf positions where a form boundary falls between leaf edges. */
62
82
  splits: Set<number>;
83
+ /** Leaf-parent (chunk) start positions from the query's OWN perceived
84
+ * fold — the positions the fold itself chose as a grouping boundary, as
85
+ * opposed to an offset a byte-level scan merely happens to land on. The
86
+ * one boundary signal opportunistic cross-leaf recovery (recognition's
87
+ * own canonical chains, the search's `fuse`) can lean on instead of
88
+ * ASCII/word heuristics: see the `boundary` gate in recognition.ts. */
89
+ starts: Set<number>;
63
90
  }
64
91
 
65
92
  /** How the consensus climb weights a region's Document-Frequency reach. */
@@ -74,6 +101,16 @@ export interface Attention {
74
101
  /** The union of the query byte-spans whose evidence supports this point. */
75
102
  start: number;
76
103
  end: number;
104
+ /** SCALE-INVARIANT confidence: the fraction of the query's OWN regions
105
+ * whose evidence this point accounts for (Σ RegionVote.absorbed among
106
+ * its contributors, over the query's total region count) — read PER-
107
+ * ANCHOR, unlike the raw IDF vote (an absolute, ln(N)-scaled quantity
108
+ * that means "strong" on a small store and "weak" on a large one for
109
+ * the SAME degree of genuine consensus). A point whose breadth clears
110
+ * `dominates` (> half the query's regions corroborate it) is real
111
+ * consensus; one that does not is a coincidental single-region echo —
112
+ * see test/35-attention-confidence.test.mjs. */
113
+ breadth: number;
77
114
  }
78
115
 
79
116
  /** Both read-outs of one consensus climb. */
@@ -111,6 +148,14 @@ export interface RegionVote {
111
148
  roots: readonly number[];
112
149
  w: number;
113
150
  wFocus: number;
151
+ /** How many of the query's ORIGINAL regions this one vote's evidence
152
+ * accounts for. 1 for an ordinary per-region vote (itself); for a
153
+ * cross-region junction vote, 1 (itself) plus however many individual
154
+ * votes it explained away (see crossRegionVotes) — the junction speaks
155
+ * for all of them at once, and breadth accounting must not undercount it
156
+ * to "one region" just because it collapsed to one pooled axiom.
157
+ * Defaults to 1 when absent. */
158
+ absorbed?: number;
114
159
  }
115
160
 
116
161
  /** The edge-bearing contexts reached by climbing from a node, plus saturation info. */
@@ -189,15 +234,19 @@ export interface MindContext extends GraphSearchHost {
189
234
  * never invalidated. Bounded LRU (byte-sized); a miss only re-perceives,
190
235
  * never a correctness risk. */
191
236
  _gistCache: BoundedMap<number, Vec>;
192
- /** DEPOSIT-path stable-prefix tree cache: content key (latin1) of a
193
- * deposited input → its plain-fold level PYRAMID, so the NEXT deposit
194
- * whose prefix it is (a conversation's accumulated context) reuses every
195
- * full aligned block and refolds only the right edge of each level
196
- * O(turn) per deposit instead of O(context), with the tree BIT-IDENTICAL
197
- * to a from-scratch fold (see FoldPyramid). Purely a performance cache.
198
- * Entry-count bounded (a pyramid is ~KB/byte of content; only the few
199
- * live conversation chains matter). */
200
- _depositTrees: BoundedMap<string, FoldPyramid>;
237
+ /** DEPOSIT-path perception cache: content key (latin1) of a deposited
238
+ * input → its accumulated turn BOUNDARIES plus reusable fold state. A
239
+ * deposit whose content extends a cached entry IS a conversation context
240
+ * grown by one turn the cached length is the new boundary so it
241
+ * folds with the SAME stable-prefix fold query-time perception uses
242
+ * (structural train/inference agreement, load-bearing for recall),
243
+ * reusing every already-folded segment via `stable` (see StableFold)
244
+ * O(turn) per deposit instead of O(context). A first-seen input keeps
245
+ * the plain fold and caches its `pyramid` (see FoldPyramid). Purely a
246
+ * performance cache for the FOLD STATE; the boundaries are semantic but
247
+ * derived only from the deposit sequence itself (an evicted chain falls
248
+ * back to plain-fold behavior, exactly the pre-boundary shape). */
249
+ _depositTrees: BoundedMap<string, DepositCacheEntry>;
201
250
  /** The byte lengths present in {@link _depositTrees} — the candidate
202
251
  * prefix lengths probed (longest first). Drifts on eviction (a stale
203
252
  * length only costs a miss); cleared with the map when it outgrows the
@@ -233,6 +282,20 @@ export function liftAnswer(segs: Seg[], queryLen: number): Uint8Array | null {
233
282
 
234
283
  if (recognised.length === 1) {
235
284
  const s = segs[recognised[0]];
285
+ // A COMPUTED span's query-side width is operand digit-count, not
286
+ // evidence of how much of the query's meaning it accounts for — the
287
+ // half-dominance check below (built for a genuinely RECOGNISED learned
288
+ // form) is not a valid framing signal for it (see the `computed` field
289
+ // doc on Seg/GItem): "1000 - 421" outweighs "what is …?" by width only
290
+ // because the operands are big, not because the framing matters less.
291
+ // A LITERAL PREFIX before a computed span is unambiguous framing
292
+ // regardless of width — an arithmetic expression is never itself
293
+ // preceded by more literal computed content, so anything literal before
294
+ // it is question wording ("what is ", "compute ") to lift clear of.
295
+ // With no prefix (s.i === 0) the span is judged by the ordinary
296
+ // half-dominance rule below, which already correctly keeps a short
297
+ // trailing glue byte ("2+2." → "4.", the span dominates a 4-byte query).
298
+ if (s.computed && s.i > 0) return s.bytes;
236
299
  if (dominates(s.j - s.i, queryLen)) {
237
300
  return concatBytes(segs.map((x) => x.bytes));
238
301
  }