@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
@@ -49,32 +49,86 @@ export async function extractBySkill(ctx, query, pre) {
49
49
  if (ranked.length === 0) {
50
50
  return fail("no consensus anchor — no skill to apply");
51
51
  }
52
- let exemplar = null;
53
- let chosenAnchor = null;
54
- let skipped = 0;
55
- for (const cand of ranked) {
56
- const ex = await pre.spanShapedOf(cand.anchor);
57
- if (ex) {
58
- exemplar = ex;
59
- chosenAnchor = cand.anchor;
60
- break;
52
+ // Try ranked anchors IN ORDER until one yields a USABLE extraction — not
53
+ // merely a span-shaped exemplar, but one whose extracted span clears the
54
+ // same one-river-fold quantum (W) cover.ts's restatedSpan gate already
55
+ // treats as the floor below which byte overlap is chance, not evidence.
56
+ // isSpanShaped (spanShapedOf) is a deliberately permissive sparse-
57
+ // subsequence check — see the section note below — so it accepts exemplars
58
+ // whose relation to the query is coincidental gap-matching, not genuine
59
+ // structure. Stopping at the FIRST such exemplar let a coincidental match
60
+ // early in the ranked list win outright and read out a sub-quantum
61
+ // fragment (observed: a 3-byte "Hel" pulled from an unrelated exemplar,
62
+ // while a later ranked anchor would have read the query's own "Hello…"
63
+ // correctly). Trying further anchors when one produces nothing usable is
64
+ // the same idiom this loop already uses for non-exemplars — extended to
65
+ // cover a bad extraction, not just a structural non-match.
66
+ //
67
+ // The retry is bounded at pre.k — the SAME evidence-breadth constant every
68
+ // other consumer of a ranked list already self-limits to (resonance, the
69
+ // weave, the climb itself; see Precomputed.k's own doc comment) — not the
70
+ // full ranked list. locate()'s frame match has an EXACT-byte tier with no
71
+ // significance correction of its own (short W-byte frames are cheap to
72
+ // match by pure chance), so trying every ranked anchor turns that per-
73
+ // anchor chance into a near-certainty over enough attempts: on a pure-
74
+ // gibberish query, 170 anchors deep found an unrelated Zulu exemplar whose
75
+ // short frame happened to byte-match, producing "xyzzy pl" — a coincidence
76
+ // no different in kind from the "RaBitQ estimate overshot the reach bar
77
+ // and grounded pure gibberish" failure recall.ts's own significance
78
+ // correction exists to prevent (see recallByResonance's reach-threshold
79
+ // comment). Bounding the search to the ranked list's own top-k restores
80
+ // the "genuinely relevant but not root-significant" exemplars this loop
81
+ // was built for, without the unbounded tail's chance collisions.
82
+ const W = ctx.space.maxGroup;
83
+ const searched = ranked.slice(0, pre.k);
84
+ let shapeMisses = 0;
85
+ let subQuantum = 0;
86
+ for (const cand of searched) {
87
+ const exemplar = await pre.spanShapedOf(cand.anchor);
88
+ if (!exemplar) {
89
+ shapeMisses++;
90
+ continue;
91
+ }
92
+ const built = buildFromExemplar(ctx, query, pre, exemplar);
93
+ if (built === null || built.bytes.length < W) {
94
+ subQuantum++;
95
+ continue;
96
+ }
97
+ if (shapeMisses > 0 || subQuantum > 0) {
98
+ ctx.trace?.step("trySkillAnchors", [
99
+ rItem(query.subarray(0, 0), `skipped ${shapeMisses + subQuantum}`),
100
+ rNode(ctx, cand.anchor, "chosen"),
101
+ ], [], `skipped ${shapeMisses} non-exemplar and ${subQuantum} sub-quantum ` +
102
+ `anchor(s) before one yielded a usable extraction`);
61
103
  }
62
- skipped++;
104
+ t?.done([rItem(built.bytes, "extracted")], built.pieces === 1
105
+ ? `apply a learnt extraction skill — read the analogous span of the query` +
106
+ ` framed like "${decodeText(exemplar.answerBytes)}" sits in its exemplar`
107
+ : `apply a learnt MULTI-PIECE skill — read ${built.pieces} analogous` +
108
+ ` pieces of the query and synthesize them like "${decodeText(exemplar.answerBytes)}"`);
109
+ return {
110
+ bytes: built.bytes,
111
+ accounted: built.accounted,
112
+ unexplained: unexplainedLabel(query, built.accounted),
113
+ };
63
114
  }
64
- if (exemplar === null) {
65
- ctx.trace?.step("trySkillAnchors", [], [], `none of ${ranked.length} ranked anchor(s) is a span-shaped skill exemplar`);
115
+ if (shapeMisses === searched.length) {
116
+ ctx.trace?.step("trySkillAnchors", [], [], `none of the top ${searched.length} ranked anchor(s) (of ${ranked.length} total) ` +
117
+ `is a span-shaped skill exemplar`);
66
118
  return fail("no consensus root is a span-shaped skill exemplar");
67
119
  }
68
- if (skipped > 0) {
69
- ctx.trace?.step("trySkillAnchors", [
70
- rItem(query.subarray(0, 0), `skipped ${skipped}`),
71
- rNode(ctx, chosenAnchor, "chosen"),
72
- ], [], `skipped ${skipped} anchor(s) that are not skill exemplars`);
73
- }
120
+ return fail("no ranked anchor yielded an extraction at or above the quantum floor");
121
+ }
122
+ /** Build the extracted bytes for ONE already-accepted span-shaped exemplar —
123
+ * factored out of {@link extractBySkill} so its anchor loop can try
124
+ * successive ranked candidates instead of committing to the first
125
+ * structural match. Null when the exemplar's answer does not decompose
126
+ * against its context, or no piece's frame locates in the query. */
127
+ function buildFromExemplar(ctx, query, pre, exemplar) {
74
128
  const { contextBytes, answerBytes } = exemplar;
75
129
  const ansCtxRuns = answerRunsInContext(ctx, contextBytes, answerBytes);
76
130
  if (ansCtxRuns === null || ansCtxRuns.length === 0) {
77
- return fail("answer is not a subsequence of the context");
131
+ return null;
78
132
  }
79
133
  if (ansCtxRuns.length > 1) {
80
134
  ctx.trace?.step("decomposeAnswer", [rItem(answerBytes, "multi-piece-answer")], ansCtxRuns.map((r) => rItem(contextBytes.subarray(r.start, r.end), "piece", undefined, [
@@ -142,19 +196,10 @@ export async function extractBySkill(ctx, query, pre) {
142
196
  accounted.push([start, end]);
143
197
  }
144
198
  if (pieces.length === 0) {
145
- return fail("no answer piece's frame located in the query");
199
+ return null;
146
200
  }
147
201
  const out = pieces.length === 1 ? pieces[0] : concatBytes(pieces);
148
- t?.done([rItem(out, "extracted")], pieces.length === 1
149
- ? `apply a learnt extraction skill — read the analogous span of the query` +
150
- ` framed like "${decodeText(answerBytes)}" sits in its exemplar`
151
- : `apply a learnt MULTI-PIECE skill — read ${pieces.length} analogous` +
152
- ` pieces of the query and synthesize them like "${decodeText(answerBytes)}"`);
153
- return {
154
- bytes: out,
155
- accounted,
156
- unexplained: unexplainedLabel(query, accounted),
157
- };
202
+ return { bytes: out, accounted, pieces: pieces.length };
158
203
  }
159
204
  // ── The two span-shape readings: OPEN acceptance vs. STRONG decomposition ──
160
205
  //
@@ -74,10 +74,34 @@ export async function recallByResonance(ctx, query, pre) {
74
74
  maximal.push(s);
75
75
  maxEnd = s.end;
76
76
  }
77
- if (maximal.length === 1) {
77
+ // The wrapper must actually BE scaffolding: RC8's own premise is "the
78
+ // wrapper is scaffolding; the argument is the span that leads
79
+ // somewhere" ("How do you say 'thank you' in French?" — everything
80
+ // outside the argument is a small fixed template). When the query
81
+ // instead has ANOTHER substantial recognised form (≥ W2, the same
82
+ // constituent bar the argument itself must clear) sitting OUTSIDE the
83
+ // chosen argument, the query is not one argument in a wrapper — it is
84
+ // several complete, independently-meaningful pieces (a multi-turn
85
+ // conversation's own accumulated turns are exactly this shape), and
86
+ // binding to the argument's continuation would answer past content
87
+ // the query itself already carries forward. Derived from the same W2
88
+ // bar the argument itself is held to, never a separate tuned number.
89
+ const hasSubstantialOutside = maximal.length === 1 &&
90
+ pre.rec.sites.some((s) => s.end - s.start >= W2 &&
91
+ (s.end <= maximal[0].start || s.start >= maximal[0].end));
92
+ if (maximal.length === 1 && !hasSubstantialOutside) {
78
93
  const arg = maximal[0];
79
94
  const g = await follow(ctx, arg.payload, queryGist);
80
- if (g !== null && g.length > 0) {
95
+ // The same "no restated fragment" guard tier 2 applies below (§ "the
96
+ // anchor cleared the consensus floor..."): a followed continuation
97
+ // that is itself a proper byte-subspan of the QUERY restates part of
98
+ // the question — never an answer. A multi-turn query's own later
99
+ // turns are exact, content-addressed matches for exactly this reason
100
+ // (each turn is its own previously-learnt form), so without this
101
+ // guard the argument's OWN later restatement in the same
102
+ // conversation reads as if it were the next thing to say.
103
+ if (g !== null && g.length > 0 &&
104
+ !(g.length < query.length && indexOf(query, g, 0) >= 0)) {
81
105
  return ground(g, "argument binding — the query's sole edge-source constituent, continuation followed", [[arg.start, arg.end]], STEP);
82
106
  }
83
107
  }
@@ -1,7 +1,7 @@
1
1
  import { Vec } from "../vec.js";
2
2
  import { Sema, Space } from "../sema.js";
3
3
  import { Alphabet } from "../alphabet.js";
4
- import { type FoldPyramid, Grid } from "../geometry.js";
4
+ import { Grid } from "../geometry.js";
5
5
  import { BoundedMap, type Store } from "../store.js";
6
6
  import { type MindConfig } from "../config.js";
7
7
  import { type Canon } from "../canon.js";
@@ -111,7 +111,7 @@ export declare class Mind implements MindContext {
111
111
  * {@link MindContext._gistCache}. 32 MB ≈ 8K gists at D=1024; hub
112
112
  * candidate sets (√N at most) fit comfortably and recur across queries. */
113
113
  _gistCache: BoundedMap<number, Vec>;
114
- _depositTrees: BoundedMap<string, FoldPyramid>;
114
+ _depositTrees: BoundedMap<string, import("./types.js").DepositCacheEntry>;
115
115
  _depositLens: Set<number>;
116
116
  _internIds: WeakMap<Sema, number>;
117
117
  private _nextConvId;
@@ -122,6 +122,7 @@ export declare class Mind implements MindContext {
122
122
  sites: ReadonlyArray<Site>;
123
123
  leaves: ReadonlyArray<Leaf>;
124
124
  splits: ReadonlySet<number>;
125
+ starts: ReadonlySet<number>;
125
126
  };
126
127
  /** Disambiguate among multiple learnt continuations of the same context node.
127
128
  * Required by {@link GraphSearchHost} — the graph search calls this through the
@@ -10,7 +10,7 @@
10
10
  // Implementation split across src/mind/*.ts — this file assembles the Mind class.
11
11
  import { makeKeyring, rng, setVecConfig } from "../vec.js";
12
12
  import { Alphabet } from "../alphabet.js";
13
- import { bytesToTreePyramid, reachThreshold, } from "../geometry.js";
13
+ import { bytesToTree, reachThreshold, } from "../geometry.js";
14
14
  import { BoundedMap } from "../store.js";
15
15
  import { SQliteStore } from "../store-sqlite.js";
16
16
  import { resolveConfig } from "../config.js";
@@ -95,7 +95,12 @@ export class Mind {
95
95
  // recogniseSpan wraps recognise
96
96
  recogniseSpan(bytes) {
97
97
  const r = recognise(this, bytes);
98
- return { sites: r.sites, leaves: r.leaves, splits: r.splits };
98
+ return {
99
+ sites: r.sites,
100
+ leaves: r.leaves,
101
+ splits: r.splits,
102
+ starts: r.starts,
103
+ };
99
104
  }
100
105
  /** Disambiguate among multiple learnt continuations of the same context node.
101
106
  * Required by {@link GraphSearchHost} — the graph search calls this through the
@@ -263,14 +268,13 @@ export class Mind {
263
268
  * where one turn ends and the next begins. */
264
269
  beginConversation(state) {
265
270
  const id = this._nextConvId++;
266
- // Build the initial pyramid: from scratch for a new conversation,
267
- // or from the saved context bytes when restoring.
268
271
  const initBytes = state?.context ?? new Uint8Array(0);
269
- const { pyramid } = bytesToTreePyramid(this.space, this.alphabet, initBytes);
272
+ const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
273
+ const tree = bytesToTree(this.space, this.alphabet, initBytes, undefined, undefined, initBoundaries.length > 0 ? initBoundaries : undefined);
270
274
  this._conversations.set(id, {
271
- pyramid,
275
+ tree,
272
276
  bytes: initBytes,
273
- boundaries: state?.boundaries ? [...state.boundaries] : [],
277
+ boundaries: initBoundaries,
274
278
  perceiveMemo: new Map(),
275
279
  recogniseMemo: new Map(),
276
280
  climbMemo: new Map(),
@@ -316,21 +320,18 @@ export class Mind {
316
320
  * place a context grows ({@link addTurn} and {@link respondTurn} both
317
321
  * come through here), so the append semantics cannot drift. */
318
322
  _growContext(data, turnBytes) {
319
- const prevLen = data.pyramid.bytes;
323
+ const prevLen = data.bytes.length;
320
324
  // An empty turn neither grows the context nor marks a boundary —
321
325
  // boundaries are documented strictly increasing, and a zero-length
322
- // "turn" is no turn. The pyramid's zero-growth shortcut makes the
323
- // refold below O(1), so the existing tree is returned unchanged.
326
+ // "turn" is no turn. Nothing changed, so the existing tree stands.
324
327
  const grow = turnBytes.length > 0;
325
- const grown = !grow
326
- ? data.bytes
327
- : prevLen > 0
328
- ? concat2(data.bytes, turnBytes)
329
- : turnBytes;
330
- if (grow && prevLen > 0)
328
+ if (!grow)
329
+ return data.tree;
330
+ const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
331
+ if (prevLen > 0)
331
332
  data.boundaries.push(prevLen);
332
- const { tree, pyramid } = bytesToTreePyramid(this.space, this.alphabet, grown, data.pyramid);
333
- data.pyramid = pyramid;
333
+ const tree = bytesToTree(this.space, this.alphabet, grown, undefined, undefined, data.boundaries.length > 0 ? data.boundaries : undefined);
334
+ data.tree = tree;
334
335
  data.bytes = grown;
335
336
  data.perceiveMemo.set(latin1Key(grown), tree);
336
337
  return tree;
@@ -377,42 +378,15 @@ export class Mind {
377
378
  this.canon = this._canonFor(typeof turn === "string" ? textCanon : null);
378
379
  this.canonMemo = this.canon ? new Map() : null;
379
380
  try {
380
- // Seed the recognise memo with the STANDALONE recognition of the
381
- // NEW turn, offset into the accumulated context. In the full-context
382
- // tree, content-defined chunk boundaries shift when bytes are
383
- // concatenated — foldTree no longer visits the original turn's root
384
- // node, so the structural pass misses it. Perceiving the turn alone
385
- // preserves intact structure. We seed ONLY the suffix (the new turn);
386
- // prefix forms stay visible through the full-context recognition
387
- // (they serve as context, not as questions to re-answer).
388
- if (data.boundaries.length > 0) {
389
- const suffixStart = data.boundaries[data.boundaries.length - 1];
390
- if (suffixStart < newContext.length) {
391
- const suffixBytes = newContext.subarray(suffixStart);
392
- const suffixRec = recognise(this, suffixBytes);
393
- const fullRec = recognise(this, newContext);
394
- const seen = new Set(fullRec.sites.map((s) => `${s.start},${s.end}`));
395
- const mergedSites = [...fullRec.sites];
396
- for (const site of suffixRec.sites) {
397
- const absStart = suffixStart + site.start;
398
- const absEnd = suffixStart + site.end;
399
- const key = `${absStart},${absEnd}`;
400
- if (!seen.has(key)) {
401
- seen.add(key);
402
- mergedSites.push({
403
- start: absStart,
404
- end: absEnd,
405
- payload: site.payload,
406
- });
407
- }
408
- }
409
- data.recogniseMemo.set(latin1Key(newContext), {
410
- sites: mergedSites,
411
- leaves: fullRec.leaves,
412
- splits: fullRec.splits,
413
- });
414
- }
415
- }
381
+ // No recognise-memo pre-seeding here: that used to be necessary
382
+ // because the flat/positional fold lost visibility into an earlier
383
+ // turn's own structure once later bytes shifted its position
384
+ // (foldTree no longer visited the turn's root node). The
385
+ // STABLE-PREFIX fold (see {@link ConversationData}) makes every
386
+ // turn's subtree independent of what follows it by construction, so
387
+ // recognise() finds it correctly on its own, first-touch, exactly
388
+ // once per turn no workaround needed, and none pre-empting
389
+ // recognise()'s own memo with a partial result.
416
390
  const top = this.trace?.enter("respondTurn", [
417
391
  rItem(newContext, "query"),
418
392
  ]);
@@ -177,8 +177,40 @@ export async function think(ctx, query, mechs) {
177
177
  ? new Set()
178
178
  : new Set(recognise(ctx, answer).sites.map((s) => s.payload));
179
179
  const reasoned = await reason(ctx, query, answer, preConsumed, pre);
180
- const fused = provenance === "recall" || provenance === "recall-echo"
181
- ? await fuseAttention(ctx, query, reasoned, pre)
180
+ // Fuse only when the query has a genuine REMAINDER no mechanism's
181
+ // structural evidence touched at all. `decided.accounted` alone
182
+ // undercounts this: it is a COST-LADDER quantity (cover.ts prices its
183
+ // masked/computed spans at near-zero and deliberately leaves them out of
184
+ // `accounted` so PASS-bridged bytes are still charged), not a coverage
185
+ // one — a query fully explained by one computed span plus bridged
186
+ // connectors can report `accounted: []` while nothing is actually left
187
+ // unexplained. The genuine remainder is what NEITHER the winning
188
+ // candidate's accounted spans NOR any recognised extension's computed
189
+ // span (`pre.computed` — every mechanism's parse() output, ALU included)
190
+ // ever touched. A remainder under one river-fold quantum (W, the same
191
+ // floor cover.ts's restatedSpan and the honesty-density bar above both
192
+ // use) is bridging punctuation/whitespace, never a second topic —
193
+ // observed: a single space between two fully-computed arithmetic spans
194
+ // ("2+2 3+3") registered as "unaccounted" and pulled in an unrelated
195
+ // corpus fact, corrupting "4 6" into "4 63".
196
+ const explained = [
197
+ ...decided.accounted,
198
+ ...pre.computed.map((u) => [u.i, u.j]),
199
+ ];
200
+ const remainder = unaccounted(explained);
201
+ // Whether the winning candidate's entire recognised substance is
202
+ // COMPUTED — every accounted span exactly a pre.computed span, nothing
203
+ // from a genuinely recognised/climbed site. fuseAttention's lone-root
204
+ // shortcut assumes a single point of attention already IS primary's own
205
+ // source; that assumption is exactly backwards for a pure computation
206
+ // (an ALU result has no anchor of its own) — see fuseAttention's
207
+ // `unclimbed` parameter, gated there by Attention.breadth so a
208
+ // coincidental echo (which this flag alone cannot distinguish) is still
209
+ // rejected.
210
+ const unclimbed = decided.accounted.length > 0 &&
211
+ decided.accounted.every(([i, j]) => pre.computed.some((u) => u.i === i && u.j === j));
212
+ const fused = remainder >= ctx.space.maxGroup
213
+ ? await fuseAttention(ctx, query, reasoned, pre, unclimbed)
182
214
  : reasoned;
183
215
  done(fused, "grounded, reasoned forward, fused across points of attention");
184
216
  return { bytes: fused, provenance };
@@ -15,15 +15,22 @@ export declare function latin1Key(bytes: Uint8Array): string;
15
15
  * CALLER — who assembled the multi-turn context — knows where those
16
16
  * boundaries are; the geometry never guesses them from the bytes. */
17
17
  export declare function perceive(ctx: MindContext, input: Input, leafAt?: (i: number) => number | null, lookup?: (ids: number[]) => number | null, boundaries?: readonly number[]): Sema;
18
- /** The DEPOSIT-shaped perceive: the PLAIN fold (bit-identical to inference
19
- * perception that structural train/inference agreement is load-bearing
20
- * for exact recall), computed INCREMENTALLY via the fold's level pyramid
21
- * ({@link bytesToTreePyramid}). An accumulated context (a conversation)
22
- * grows by suffixes: the previous context's pyramid is cached by CONTENT
23
- * (ctx._depositTrees), and this deposit refolds only the right edge of
24
- * each level O(turn) instead of O(context) per turn. Purely a cache:
25
- * the produced tree never depends on cache state. */
26
- export declare function perceiveDeposit(ctx: MindContext, bytes: Uint8Array): Sema;
18
+ /** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
19
+ * (bit-identical to inference perception of a standalone query that
20
+ * structural train/inference agreement is load-bearing for exact recall),
21
+ * computed incrementally via the fold's level pyramid
22
+ * ({@link bytesToTreePyramid}). An input that EXTENDS a previously
23
+ * deposited one is a conversation context grown by one turn — the cached
24
+ * prefix length IS the turn boundary (derived from the deposit sequence
25
+ * itself, never from content conventions) and takes the STABLE-PREFIX
26
+ * fold over the accumulated boundaries, bit-identical to the boundary
27
+ * fold query-time conversation perception uses, so the trained context
28
+ * node and the query's context subtree are the SAME node. Segment folds
29
+ * reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
30
+ * instead of O(context) per turn. The fold state is purely a cache; the
31
+ * boundary accumulation is what an evicted chain loses (falling back to
32
+ * the plain fold, the pre-boundary shape — a warm replay restores it). */
33
+ export declare function perceiveDeposit(ctx: MindContext, bytes: Uint8Array, conversational?: boolean): Sema;
27
34
  /** The raw bytes of an input — modality-neutral conversion. */
28
35
  export declare function inputBytes(ctx: MindContext, input: Input): Uint8Array;
29
36
  /** Convenience: the gist vector of a byte span. */
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Address — bytes → node (perceive, foldTree, resolve)
4
4
  // Read — node → bytes (read)
5
- import { bytesToTree, bytesToTreePyramid, gridToTree, hilbertBytes, stackGrids, } from "../geometry.js";
5
+ import { bytesToTree, bytesToTreePyramid, gridToTree, hilbertBytes, stablePrefixFoldIncremental, stackGrids, } from "../geometry.js";
6
6
  import { canonHash } from "../canon.js";
7
7
  import { bytesEqual } from "../bytes.js";
8
8
  import { ALL } from "./types.js";
@@ -58,29 +58,65 @@ export function perceive(ctx, input, leafAt, lookup, boundaries) {
58
58
  }
59
59
  return gridToTree(ctx.space, ctx.alphabet, input);
60
60
  }
61
- /** The DEPOSIT-shaped perceive: the PLAIN fold (bit-identical to inference
62
- * perception that structural train/inference agreement is load-bearing
63
- * for exact recall), computed INCREMENTALLY via the fold's level pyramid
64
- * ({@link bytesToTreePyramid}). An accumulated context (a conversation)
65
- * grows by suffixes: the previous context's pyramid is cached by CONTENT
66
- * (ctx._depositTrees), and this deposit refolds only the right edge of
67
- * each level O(turn) instead of O(context) per turn. Purely a cache:
68
- * the produced tree never depends on cache state. */
69
- export function perceiveDeposit(ctx, bytes) {
61
+ /** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
62
+ * (bit-identical to inference perception of a standalone query that
63
+ * structural train/inference agreement is load-bearing for exact recall),
64
+ * computed incrementally via the fold's level pyramid
65
+ * ({@link bytesToTreePyramid}). An input that EXTENDS a previously
66
+ * deposited one is a conversation context grown by one turn — the cached
67
+ * prefix length IS the turn boundary (derived from the deposit sequence
68
+ * itself, never from content conventions) and takes the STABLE-PREFIX
69
+ * fold over the accumulated boundaries, bit-identical to the boundary
70
+ * fold query-time conversation perception uses, so the trained context
71
+ * node and the query's context subtree are the SAME node. Segment folds
72
+ * reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
73
+ * instead of O(context) per turn. The fold state is purely a cache; the
74
+ * boundary accumulation is what an evicted chain loses (falling back to
75
+ * the plain fold, the pre-boundary shape — a warm replay restores it). */
76
+ export function perceiveDeposit(ctx, bytes, conversational = false) {
70
77
  let prev;
71
- // Longest cached PROPER prefix first.
72
- const lens = [...ctx._depositLens]
73
- .filter((L) => L >= 2 && L < bytes.length)
74
- .sort((a, b) => b - a);
75
- for (const L of lens) {
76
- const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
77
- if (hit !== undefined) {
78
- prev = hit;
79
- break;
78
+ let prefixLen = 0;
79
+ // Cache consult (both boundary lookup and stable-prefix reuse) is scoped
80
+ // to conversational deposits only a bare, unrelated fact whose bytes
81
+ // happen to extend an earlier deposit is NOT a conversation turn, and
82
+ // must keep the plain fold so it shares structure with ITS OWN prior
83
+ // deposits, not fragment against a coincidental byte-prefix.
84
+ if (conversational) {
85
+ // Longest cached PROPER prefix first.
86
+ const lens = [...ctx._depositLens]
87
+ .filter((L) => L >= 2 && L < bytes.length)
88
+ .sort((a, b) => b - a);
89
+ for (const L of lens) {
90
+ const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
91
+ // The suffix must bytes-equal the hit's OWN recorded continuation —
92
+ // proof this deposit is that turn's actual next turn, not a fact
93
+ // that coincidentally shares its byte prefix.
94
+ if (hit !== undefined && hit.nextBytes !== undefined &&
95
+ bytesEqual(hit.nextBytes, bytes.subarray(L))) {
96
+ prev = hit;
97
+ prefixLen = L;
98
+ break;
99
+ }
80
100
  }
81
101
  }
82
- const { tree, pyramid } = bytesToTreePyramid(ctx.space, ctx.alphabet, bytes, prev);
83
- if (bytes.length >= 2) {
102
+ let tree;
103
+ let entry;
104
+ if (prev !== undefined) {
105
+ const boundaries = [...prev.boundaries, prefixLen];
106
+ const folded = stablePrefixFoldIncremental(ctx.space, ctx.alphabet, bytes, boundaries, prev.stable);
107
+ tree = folded.tree;
108
+ entry = { boundaries, stable: folded.fold };
109
+ }
110
+ else {
111
+ const plain = bytesToTreePyramid(ctx.space, ctx.alphabet, bytes);
112
+ tree = plain.tree;
113
+ entry = { boundaries: [], pyramid: plain.pyramid };
114
+ }
115
+ // Only a conversational deposit writes the cache too — otherwise a bare
116
+ // fact's plain fold could later be misread as a conversation's turn-zero
117
+ // boundary by an unrelated conversational deposit that happens to extend
118
+ // its bytes.
119
+ if (conversational && bytes.length >= 2) {
84
120
  // The lengths set drifts as the map evicts; past the probe budget the
85
121
  // drift itself becomes the cost (each stale length is an O(len) key
86
122
  // build), so both reset together — losing only warm-up on live chains.
@@ -88,7 +124,7 @@ export function perceiveDeposit(ctx, bytes) {
88
124
  ctx._depositLens.clear();
89
125
  ctx._depositTrees.clear();
90
126
  }
91
- ctx._depositTrees.set(latin1Key(bytes), pyramid);
127
+ ctx._depositTrees.set(latin1Key(bytes), entry);
92
128
  ctx._depositLens.add(bytes.length);
93
129
  }
94
130
  return tree;
@@ -12,4 +12,12 @@ export declare function reason(ctx: MindContext, query: Uint8Array, answer: Uint
12
12
  * When the consensus climb finds more than one dominant point, each
13
13
  * independent point grounds its own answer; they are bridged together
14
14
  * by any learnt connector the graph holds between them. */
15
- export declare function fuseAttention(ctx: MindContext, query: Uint8Array, primary: Uint8Array, pre: Precomputed): Promise<Uint8Array>;
15
+ export declare function fuseAttention(ctx: MindContext, query: Uint8Array, primary: Uint8Array, pre: Precomputed,
16
+ /** True when `primary` never touched the consensus climb at all — e.g. a
17
+ * pure ALU computation, which has no anchor of its own. commitVotes
18
+ * ALWAYS admits the dominant root regardless of its vote (attention.ts:
19
+ * "roots.length === 0 || …") on the assumption a lone root already IS
20
+ * primary's own source; that assumption is exactly backwards when
21
+ * primary is unclimbed. Absent or false preserves the original
22
+ * behaviour exactly. */
23
+ unclimbed?: boolean): Promise<Uint8Array>;
@@ -111,7 +111,15 @@ export async function reason(ctx, query, answer, preConsumed, pre) {
111
111
  * When the consensus climb finds more than one dominant point, each
112
112
  * independent point grounds its own answer; they are bridged together
113
113
  * by any learnt connector the graph holds between them. */
114
- export async function fuseAttention(ctx, query, primary, pre) {
114
+ export async function fuseAttention(ctx, query, primary, pre,
115
+ /** True when `primary` never touched the consensus climb at all — e.g. a
116
+ * pure ALU computation, which has no anchor of its own. commitVotes
117
+ * ALWAYS admits the dominant root regardless of its vote (attention.ts:
118
+ * "roots.length === 0 || …") on the assumption a lone root already IS
119
+ * primary's own source; that assumption is exactly backwards when
120
+ * primary is unclimbed. Absent or false preserves the original
121
+ * behaviour exactly. */
122
+ unclimbed = false) {
115
123
  // When the answer is structurally drawn from the query itself
116
124
  // (extraction), it already spans all the query's pieces — fusion
117
125
  // would only add noise from unrelated stored contexts. The gate is
@@ -125,17 +133,31 @@ export async function fuseAttention(ctx, query, primary, pre) {
125
133
  // query, same k, same DF mode) — read them from Precomputed instead of
126
134
  // re-climbing, so even a traced response pays for the climb once.
127
135
  const forest = (await pre.attention()).roots;
128
- if (forest.length <= 1)
136
+ // A LONE root is ordinarily primary's own source — nothing to fuse. But
137
+ // when primary is unclimbed, the lone root was never checked against
138
+ // anything: it is admitted by commitVotes unconditionally, so it may be
139
+ // genuine consensus (Attention.breadth dominates — most of the query's
140
+ // OWN regions corroborate it) or a coincidental echo (breadth does not
141
+ // dominate — see test/35-attention-confidence). breadth is the SCALE-
142
+ // INVARIANT read of exactly this question: the raw IDF vote cannot serve
143
+ // here, since it is an absolute ln(N)-scaled quantity (a genuine root on
144
+ // a large store can score BELOW its own floor while a coincidental echo
145
+ // on a small one scores comfortably above its own, smaller, floor).
146
+ const lonePromotes = unclimbed && forest.length === 1 &&
147
+ forest[0].breadth > 0.5;
148
+ if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) {
129
149
  return primary;
150
+ }
130
151
  const pieces = [
131
152
  { start: forest[0].start, bytes: primary },
132
153
  ];
133
154
  const qv = pre.guide; // once, not per root
155
+ const rest = lonePromotes ? forest : forest.slice(1);
134
156
  const t = ctx.trace?.enter("fuseAttention", [
135
157
  rItem(primary, "primary"),
136
- ...forest.slice(1).map((r) => rNode(ctx, r.anchor, "point", r.vote)),
158
+ ...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
137
159
  ]);
138
- for (const root of forest.slice(1)) {
160
+ for (const root of rest) {
139
161
  const g = await project(ctx, root.anchor, qv);
140
162
  if (g === null || g.length === 0)
141
163
  continue;