@hviana/sema 0.2.0 → 0.2.2

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.
@@ -90,6 +90,13 @@ export declare function consensusFloor(N: number): number;
90
90
  * replaces runtime coverage gating with a batch pass that removes
91
91
  * structurally-isolated entries. Derived, never tuned. */
92
92
  export declare function coverageBar(_maxGroup: number, D: number): number;
93
+ export interface Folded {
94
+ tree: Sema;
95
+ /** Byte length of the subtree — carried incrementally so the stable-prefix
96
+ * boundary scan never re-walks subtrees (the old per-level walk was
97
+ * O(n log n) over the whole input). */
98
+ len: number;
99
+ }
93
100
  export interface Grid {
94
101
  width: number;
95
102
  height: number;
@@ -116,6 +123,27 @@ export declare function knownPrefixLength(bytes: Uint8Array, leafAt: (i: number)
116
123
  * turn extend perception instead of refolding it: identical prefixes
117
124
  * produce identical subtrees regardless of what follows them. */
118
125
  export declare function bytesToTree(space: Space, alphabet: Alphabet, bytes: Uint8Array, leafAt?: (i: number) => number | null, lookup?: (leafIds: number[]) => number | null, boundaries?: readonly number[]): Sema;
126
+ /** A stable-prefix fold's reusable state: the segment edge offsets and each
127
+ * segment's independently-folded root ({@link riverFoldRaw} output). A
128
+ * grown stream whose boundary set EXTENDS a previous fold's reuses every
129
+ * matching segment's Folded unchanged (segments fold independently by
130
+ * construction, so reuse is bit-identical to refolding) and folds only the
131
+ * new right-edge segment — O(turn) per extension, the stable-prefix
132
+ * counterpart of {@link FoldPyramid}. Purely a cache: the produced tree
133
+ * never depends on cache state. */
134
+ export interface StableFold {
135
+ edges: number[];
136
+ segs: Folded[];
137
+ }
138
+ /** {@link stablePrefixFold} with incremental segment reuse — same cuts, same
139
+ * segment folds, same left-nested join, same single root normalize; `prev`
140
+ * only elides recomputing segments whose [start,end) offsets it already
141
+ * folded over a byte-identical prefix (the caller keys the cache by
142
+ * content). Requires a non-empty effective boundary set. */
143
+ export declare function stablePrefixFoldIncremental(space: Space, alphabet: Alphabet, bytes: Uint8Array, boundaries: readonly number[], prev?: StableFold): {
144
+ tree: Sema;
145
+ fold: StableFold;
146
+ };
119
147
  /** The PLAIN fold's full level pyramid — every level's item list, bottom
120
148
  * (leaves) to top (root). Left-grouped folding is RADIX-ALIGNED: the item
121
149
  * at level L, index i, covers exactly bytes [i·mg^L, (i+1)·mg^L) whenever
@@ -321,6 +321,41 @@ function stablePrefixFold(space, alphabet, bytes, boundaries) {
321
321
  normalize(cur.tree.v);
322
322
  return cur.tree;
323
323
  }
324
+ /** {@link stablePrefixFold} with incremental segment reuse — same cuts, same
325
+ * segment folds, same left-nested join, same single root normalize; `prev`
326
+ * only elides recomputing segments whose [start,end) offsets it already
327
+ * folded over a byte-identical prefix (the caller keys the cache by
328
+ * content). Requires a non-empty effective boundary set. */
329
+ export function stablePrefixFoldIncremental(space, alphabet, bytes, boundaries, prev) {
330
+ const cuts = [];
331
+ let prevB = 0;
332
+ for (const b of boundaries) {
333
+ if (b > prevB && b < bytes.length) {
334
+ cuts.push(b);
335
+ prevB = b;
336
+ }
337
+ }
338
+ const edges = [0, ...cuts, bytes.length];
339
+ const segs = [];
340
+ for (let i = 0; i + 1 < edges.length; i++) {
341
+ const hit = prev !== undefined && prev.edges[i] === edges[i] &&
342
+ prev.edges[i + 1] === edges[i + 1]
343
+ ? prev.segs[i]
344
+ : undefined;
345
+ segs.push(hit ??
346
+ riverFoldRaw(space, bytesToLeaves(alphabet, bytes.subarray(edges[i], edges[i + 1]))));
347
+ }
348
+ if (segs.length === 1) {
349
+ // Degenerate boundary set — the plain fold, as stablePrefixFold does.
350
+ const tree = riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
351
+ return { tree, fold: { edges, segs } };
352
+ }
353
+ let cur = segs[0];
354
+ for (let i = 1; i < segs.length; i++)
355
+ cur = fold2(space, cur, segs[i]);
356
+ normalize(cur.tree.v);
357
+ return { tree: cur.tree, fold: { edges, segs } };
358
+ }
324
359
  /** Join two folded items as one 2-kid branch — the top-level join of the
325
360
  * stable-prefix fold, identical FP ops to foldSlice's seat-bound
326
361
  * accumulation over a group of two. Unnormalized (interior). */
@@ -736,6 +736,27 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
736
736
  continue;
737
737
  if (ra.end >= rb.start)
738
738
  continue; // overlap or adjacent — nothing between
739
+ // Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
740
+ // that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
741
+ // genuine, individually-corroborated evidence about what fills the gap
742
+ // — gate the container search below: a joint container is binding
743
+ // evidence only when it is CONSISTENT with that evidence, i.e. its own
744
+ // bytes actually contain what the between-region says. This is the
745
+ // n-ary composition's normal shape (a between-attribute's bytes DO
746
+ // recur inside the joint container, credited as an "extra" below) as
747
+ // opposed to a container that silently substitutes something else for
748
+ // it (e.g. bridging past "Italy" to a container whose interior is
749
+ // "Japan" — a different, contradicting learnt whole).
750
+ // Only a KNOWN (content-addressed, exact) between-region qualifies —
751
+ // an approximate region's resonance climbing "somewhere" is ordinary
752
+ // noise (any ANN query returns SOME nearest neighbour), not evidence
753
+ // this specific gap already means something specific.
754
+ const between = [];
755
+ for (let m = a + 1; m < b; m++) {
756
+ if (strong.has(cand[m]) && !consumed.has(cand[m]) &&
757
+ regions[cand[m]].known)
758
+ between.push(cand[m]);
759
+ }
739
760
  // A single KNOWN region covering both: the whole form is already a
740
761
  // stored identity that votes directly; its pieces add nothing.
741
762
  if (regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end))
@@ -781,6 +802,14 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
781
802
  if (indexOf(query, joined, 0) >= 0)
782
803
  continue; // query says it itself
783
804
  }
805
+ // CONTRADICTION GUARD: a between-region already carrying its own
806
+ // vote must actually recur in this container's bytes — otherwise
807
+ // the container is a different learnt whole that happens to share
808
+ // ra/rb, and letting it stand in for the gap would silently
809
+ // override evidence the query itself already resolved there.
810
+ if (between.some((bi) => indexOf(bytes, query.subarray(regions[bi].start, regions[bi].end), 0) < 0)) {
811
+ continue;
812
+ }
784
813
  let cov = left.length + right.length;
785
814
  const extras = [];
786
815
  for (const ei of cand) {
@@ -487,53 +487,104 @@ export class GraphSearch {
487
487
  }
488
488
  return;
489
489
  }
490
- // LIMIT 2 decides all three facts this rule needs — emptiness, plurality
491
- // (whether to consult the disambiguator), and the first-inserted
492
- // fallback without materialising a hub context's corpus-sized fan-out.
493
- const nx = this.store.nextFirst(it.node, 2);
494
- if (nx.length) {
495
- // A direct edge step along the chain toward its fixpoint. A recomposed
496
- // form (parts already rewritten and fused into a learned whole) follows
497
- // its continuation at MICRO, so reaching the grounded answer of the
498
- // recomposition beats leaving the parts split; the flag rides the chain so
499
- // every step of the recomposition's completion stays cheap.
500
- //
501
- // WHICH edge: a context node often carries SEVERAL learnt continuations
502
- // (the same sentence trained against 100+ target languages, a question
503
- // answered differently across sessions). `nx[0]` is an accident of
504
- // training order; when the host provides a `chooseNext` disambiguator
505
- // (see Mind.chooseNext) it picks the continuation with the most
506
- // distributional evidence (prevOf count the structural manifestation
507
- // of its halo), falling back to first-inserted when evidence is equal.
508
- yield {
509
- premises: [it],
510
- conclusion: {
511
- kind: "form",
512
- i: it.i,
513
- j: it.j,
514
- node: (nx.length > 1 ? this.host.chooseNext?.(it.node) : undefined) ??
515
- nx[0],
516
- via: true,
517
- rcmp: it.rcmp,
518
- },
519
- // A recomposed form's continuation is FREE: the two (or more) parts were
520
- // already paid for as their own rewrites, and the single consolidated
521
- // span saves one cover-bridge versus leaving them split so charging 0
522
- // here makes the grounded whole (e.g. "DE"→F) strictly beat the split
523
- // ("D","E") by exactly that saved bridge, deterministically.
524
- cost: it.rcmp ? 0 : STEP,
525
- };
526
- }
527
- else if (it.via) {
528
- // The chain reached a node with no WHOLE-node continuation. Before
529
- // emitting it as terminal, CONTINUE THE EXPLORATION into its own structure:
530
- // a composite answer like "p1 p2" leads nowhere as a whole, yet recognising
531
- // it surfaces p1, p2 — each of which continues ( R1, R2) and recomposes
532
- // into a deeper learnt form (→ FINAL). {@link recompleteNode} re-covers the
533
- // node's bytes through the SAME recognition + edge/fuse machinery the top
534
- // query uses (a continued graph exploration, not a re-perception), and
535
- // returns the deeper completion's bytes when it genuinely leads somewhere
536
- // new. Emit that; else emit the node itself as the terminal answer.
490
+ if (it.via) {
491
+ // A CHAIN hop (this form was itself reached by following an edge):
492
+ // read up to the hub bound the SAME √(max(2,N)) convention {@link
493
+ // hubBound} in traverse.ts derives, replicated here because
494
+ // GraphSearch holds only a bare Store, not a MindContext — and fork
495
+ // across EVERY continuation. A bidirectional pair (SmolSent trains
496
+ // both directions) puts the back-edge to where we came from ahead of
497
+ // the forward edge in insertion order; a single first-inserted pick
498
+ // would walk the search straight into a cycle (the A*LD duplicate-key
499
+ // guard then dead-ends it) with no way to reach the forward edge.
500
+ // Forking offers every continuation as its own rule so the one that
501
+ // genuinely advances (not a duplicate) is still reachable.
502
+ const bound = Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
503
+ const nx = this.store.nextFirst(it.node, bound);
504
+ if (nx.length) {
505
+ // The SAME evidence-weighted disambiguation the first hop uses
506
+ // (below) identifies the most-corroborated continuation. Yielding
507
+ // it FIRST matters: {@link lightestDerivation}'s chart keeps the
508
+ // FIRST rule to reach a given cost for an item and discards later
509
+ // arrivals at an EQUAL cost (`cost < current`, strictly) — so
510
+ // among same-depth sibling forks that tie in cost, the
511
+ // evidence-backed edge wins deterministically, never by
512
+ // exploration-order luck. `preferred`, when set, is necessarily
513
+ // an element of `nx` (chooseNext reads the identical hub-bounded
514
+ // set see traverse.ts), so a plain skip-in-place suffices; no
515
+ // second array need be allocated to reorder it to the front.
516
+ const preferred = nx.length > 1
517
+ ? this.host.chooseNext?.(it.node)
518
+ : undefined;
519
+ const fork = (node) => ({
520
+ premises: [it],
521
+ conclusion: { kind: "form", i: it.i, j: it.j, node, via: true },
522
+ // A real edge always costs STEP, whether it is the first hop or
523
+ // the fifth so a chain's total cost is proportional to its
524
+ // length and the lightest derivation is always the SHORTEST
525
+ // successful one. (Charging every hop after the first for
526
+ // FREE, as an earlier design did, made every stopping point at
527
+ // any depth tie at the same cost — indistinguishable to the
528
+ // search, decided by whichever arrived first.)
529
+ cost: STEP,
530
+ });
531
+ if (preferred !== undefined)
532
+ yield fork(preferred);
533
+ for (const c of nx) {
534
+ if (c !== preferred)
535
+ yield fork(c);
536
+ }
537
+ // Also offer stopping HERE, at CONCEPT above whatever the chain
538
+ // has already cost — the same "a synonym jump is dearer than a
539
+ // direct edge" ordering the ladder already uses elsewhere,
540
+ // repurposed as "giving up early is dearer than one more real
541
+ // hop". A premature stop at depth D costs D·STEP + CONCEPT, so a
542
+ // SHORTER premature stop always beats a longer one, and a genuine
543
+ // fixpoint (below, costing +0) always beats any premature stop at
544
+ // the same depth — the search only settles here when continuing
545
+ // genuinely dead-ends (every fork above revisits an already-
546
+ // explored key) or grows costlier than giving up.
547
+ //
548
+ // The stop-here bytes are the node's OWN, as-is — NOT recompleteNode's
549
+ // recursive re-cover. recompleteNode re-runs the full recognition
550
+ // + edge/fuse search over the node's bytes, an O(node's own
551
+ // substructure) cost independent of chain depth; offering it at
552
+ // EVERY premature stop makes a query's total cost scale with how
553
+ // many nodes the corpus happens to interconnect here (corpus
554
+ // density), not with the answer's own hop count — the exact
555
+ // output-sensitivity the rest of this search is built to keep.
556
+ // Reserved below for the ONE place it is load-bearing: the true
557
+ // fixpoint, checked once, at the chain's actual end.
558
+ yield {
559
+ premises: [it],
560
+ conclusion: {
561
+ kind: "out",
562
+ i: it.i,
563
+ j: it.j,
564
+ bytes: nodeBytes(it.node),
565
+ cover: true,
566
+ rec: true,
567
+ node: it.node,
568
+ },
569
+ cost: CONCEPT,
570
+ };
571
+ return;
572
+ }
573
+ // The chain reached a node with no WHOLE-node continuation anywhere
574
+ // — a genuine fixpoint, not a premature stop. Before emitting it as
575
+ // terminal, CONTINUE THE EXPLORATION into its own structure: a
576
+ // composite answer like "p1 p2" leads nowhere as a whole, yet
577
+ // recognising it surfaces p1, p2 — each of which continues (→ R1,
578
+ // R2) and recomposes into a deeper learnt form (→ FINAL). {@link
579
+ // recompleteNode} re-covers the node's bytes through the SAME
580
+ // recognition + edge/fuse machinery the top query uses (a continued
581
+ // graph exploration, not a re-perception), and returns the deeper
582
+ // completion's bytes when it genuinely leads somewhere new. Emit
583
+ // that; else emit the node itself as the terminal answer. This is
584
+ // the ONE place the recursive re-cover runs — once, at the chain's
585
+ // actual end, never per intermediate stop — so its cost tracks the
586
+ // ANSWER's own structure, not how densely the corpus interconnects
587
+ // the nodes passed through on the way there.
537
588
  const deeper = this.recompleteNode(it.node);
538
589
  yield {
539
590
  premises: [it],
@@ -550,6 +601,35 @@ export class GraphSearch {
550
601
  };
551
602
  }
552
603
  else {
604
+ // The FIRST hop out of a recognised site — unchanged from the
605
+ // original design. LIMIT 2 only senses PLURALITY (does this node
606
+ // have more than one learnt continuation?); the actual bounded read
607
+ // and evidence-weighted pick both live inside {@link
608
+ // Mind.chooseNext} (see traverse.ts), which floors its own hub bound
609
+ // at √(max(2,N)) regardless of what we read here.
610
+ const nx = this.store.nextFirst(it.node, 2);
611
+ if (nx.length) {
612
+ yield {
613
+ premises: [it],
614
+ conclusion: {
615
+ kind: "form",
616
+ i: it.i,
617
+ j: it.j,
618
+ node: (nx.length > 1 ? this.host.chooseNext?.(it.node) : undefined) ??
619
+ nx[0],
620
+ via: true,
621
+ rcmp: it.rcmp,
622
+ },
623
+ // A recomposed form's continuation is FREE: the two (or more) parts
624
+ // were already paid for as their own rewrites, and the single
625
+ // consolidated span saves one cover-bridge versus leaving them
626
+ // split — so charging 0 here makes the grounded whole (e.g.
627
+ // "DE"→F) strictly beat the split ("D","E") by exactly that saved
628
+ // bridge, deterministically.
629
+ cost: it.rcmp ? 0 : STEP,
630
+ };
631
+ return;
632
+ }
553
633
  // Recognised but edge-less: borrow a concept (halo) sibling's edge. No
554
634
  // edge and no concept means the form leads nowhere — it yields no rule, so
555
635
  // a query of only such forms produces no derivation, and think is silent.
@@ -18,7 +18,7 @@ export declare function internTreeIds(ctx: MindContext, node: Sema, ids: Map<Sem
18
18
  export declare function indexSubSpans(ctx: MindContext, tree: Sema, ids: Map<Sema, number>): Promise<boolean>;
19
19
  /** Perceive, intern, and index a single input. Returns the perceived tree,
20
20
  * root id, id map, and the changed (new) subtrees for halo reinforcement. */
21
- export declare function deposit(ctx: MindContext, input: Input, track: boolean): Promise<{
21
+ export declare function deposit(ctx: MindContext, input: Input, track: boolean, conversational?: boolean): Promise<{
22
22
  tree: Sema;
23
23
  rootId: number;
24
24
  ids: Map<Sema, number>;
@@ -4,7 +4,7 @@
4
4
  // node. A fact is an EDGE between node ids; recall traverses edges.
5
5
  import { bindSeat, companySignature, isChunk } from "../sema.js";
6
6
  import { changedNodes } from "./types.js";
7
- import { inputBytes, perceiveDeposit, resolve, } from "./primitives.js";
7
+ import { inputBytes, latin1Key, perceiveDeposit, resolve, } from "./primitives.js";
8
8
  import { canonicalWindows, leafIdPrefix } from "./canonical.js";
9
9
  import { fold as foldVecs } from "../sema.js";
10
10
  /** Intern a perceived tree into node ids, bottom-up, sharing equal subtrees.
@@ -103,7 +103,7 @@ export async function indexSubSpans(ctx, tree, ids) {
103
103
  }
104
104
  /** Perceive, intern, and index a single input. Returns the perceived tree,
105
105
  * root id, id map, and the changed (new) subtrees for halo reinforcement. */
106
- export async function deposit(ctx, input, track) {
106
+ export async function deposit(ctx, input, track, conversational = false) {
107
107
  const bytes = inputBytes(ctx, input);
108
108
  // Deposit-shaped perception: stable-prefix tree SEEDING (see
109
109
  // perceiveDeposit) — an accumulated context re-folds only its new suffix,
@@ -111,8 +111,13 @@ export async function deposit(ctx, input, track) {
111
111
  // (no store-probe fallback): a knownPrefixLength scan on every novel fact
112
112
  // would cost O(n²) hashing, while conversation replays are always warm —
113
113
  // re-deposition replays from the first turn, rebuilding the cache as it
114
- // goes.
115
- const tree = perceiveDeposit(ctx, bytes);
114
+ // goes. `conversational` scopes the STABLE-PREFIX variant (turn-boundary
115
+ // folding, matching query-time perception) to ingestPair's own growing
116
+ // context argument — a bare ingestOne deposit whose bytes merely happen
117
+ // to extend an earlier UNRELATED deposit (no conversational relationship)
118
+ // must keep the plain fold, or two coincidentally-prefix-sharing facts
119
+ // would stop sharing structure with each other.
120
+ const tree = perceiveDeposit(ctx, bytes, conversational);
116
121
  const ids = new Map();
117
122
  const rootId = await internTreeIds(ctx, tree, ids);
118
123
  const indexed = await indexSubSpans(ctx, tree, ids);
@@ -185,9 +190,19 @@ async function propagateSuffixes(ctx, src, dst) {
185
190
  }
186
191
  /** Ingest a pair (context, continuation) — learn an edge and pour halos. */
187
192
  export async function ingestPair(ctx, ctxInput, cont) {
188
- const c = await deposit(ctx, ctxInput, true);
193
+ const c = await deposit(ctx, ctxInput, true, true);
189
194
  const cont_ = await deposit(ctx, cont, false);
190
195
  const ctxId = c.rootId, contId = cont_.rootId;
196
+ // Stamp this turn's continuation onto its own cache entry — the proof a
197
+ // FUTURE, longer ctxInput needs (see perceiveDeposit) to recognise itself
198
+ // as this conversation's genuine next turn rather than an unrelated fact
199
+ // that happens to share this ctxInput's byte prefix.
200
+ {
201
+ const ctxBytes = inputBytes(ctx, ctxInput);
202
+ const entry = ctx._depositTrees.get(latin1Key(ctxBytes));
203
+ if (entry !== undefined)
204
+ entry.nextBytes = inputBytes(ctx, cont);
205
+ }
191
206
  await ctx.store.link(ctxId, contId);
192
207
  await propagateSuffixes(ctx, ctxId, contId);
193
208
  // Halos pour company SIGNATURES (identity), not gists (content) — see
@@ -8,6 +8,7 @@
8
8
  // cover runs FIRST in defaultMechanisms: a computed-backed cover becomes a
9
9
  // near-zero-cost incumbent that prunes the other mechanisms through the
10
10
  // ordinary admissible-floor check, with no extension special-case anywhere.
11
+ import { bytesEqual, indexOf } from "../../bytes.js";
11
12
  import { read, resolve } from "../primitives.js";
12
13
  import { guidedFirst } from "../traverse.js";
13
14
  import { conceptHop } from "../match.js";
@@ -158,6 +159,37 @@ export const coverMechanism = {
158
159
  : "lightest derivation: the chosen spans, left to right");
159
160
  if (segs === null)
160
161
  return [];
162
+ // A chosen span's SUBSTITUTED bytes (an edge followed from a recognised
163
+ // site, not the site's own literal text read back) that equal a byte
164
+ // span the query ALREADY CONTAINS elsewhere restates part of the
165
+ // question — never an answer (the same principle recall.ts's tiers
166
+ // apply to a whole-query projection: "a projection that is a proper
167
+ // byte-subspan of the query restates part of the question"). A
168
+ // recognised site that is itself an entire PRIOR TURN of a multi-turn
169
+ // query is exactly this shape: it carries a genuine learnt
170
+ // continuation, but that continuation is something the asker already
171
+ // said moments later in the SAME query, not a new answer — the cover
172
+ // search has no notion of "turn" to gate this itself, so the check
173
+ // belongs here, over the derivation it already chose.
174
+ const W = ctx.space.maxGroup;
175
+ for (const s of segs) {
176
+ if (!s.rec)
177
+ continue;
178
+ const literal = s.j - s.i === s.bytes.length &&
179
+ bytesEqual(s.bytes, query.subarray(s.i, s.j));
180
+ if (literal)
181
+ continue;
182
+ // A span shorter than one river window is exactly the "ice"/"c"
183
+ // case: a chain hop's short terminal coincidentally recurring as a
184
+ // substring of an unrelated longer word. Below the quantum, byte
185
+ // overlap is chance, not evidence — the same floor identityBar and
186
+ // reachThreshold hold every other structural-overlap claim to.
187
+ if (s.bytes.length >= W && s.bytes.length < query.length &&
188
+ indexOf(query, s.bytes, 0) >= 0) {
189
+ ctx.trace?.step("restatedSpan", [rItem(s.bytes, "substituted", s.node, [s.i, s.j])], [], "the chosen span's substitution already occurs elsewhere in the query — restates it, not an answer");
190
+ return [];
191
+ }
192
+ }
161
193
  const composed = liftAnswer(segs, query.length);
162
194
  if (composed === null)
163
195
  return [];
@@ -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;
@@ -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";
@@ -263,14 +263,13 @@ export class Mind {
263
263
  * where one turn ends and the next begins. */
264
264
  beginConversation(state) {
265
265
  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
266
  const initBytes = state?.context ?? new Uint8Array(0);
269
- const { pyramid } = bytesToTreePyramid(this.space, this.alphabet, initBytes);
267
+ const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
268
+ const tree = bytesToTree(this.space, this.alphabet, initBytes, undefined, undefined, initBoundaries.length > 0 ? initBoundaries : undefined);
270
269
  this._conversations.set(id, {
271
- pyramid,
270
+ tree,
272
271
  bytes: initBytes,
273
- boundaries: state?.boundaries ? [...state.boundaries] : [],
272
+ boundaries: initBoundaries,
274
273
  perceiveMemo: new Map(),
275
274
  recogniseMemo: new Map(),
276
275
  climbMemo: new Map(),
@@ -316,21 +315,18 @@ export class Mind {
316
315
  * place a context grows ({@link addTurn} and {@link respondTurn} both
317
316
  * come through here), so the append semantics cannot drift. */
318
317
  _growContext(data, turnBytes) {
319
- const prevLen = data.pyramid.bytes;
318
+ const prevLen = data.bytes.length;
320
319
  // An empty turn neither grows the context nor marks a boundary —
321
320
  // 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.
321
+ // "turn" is no turn. Nothing changed, so the existing tree stands.
324
322
  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)
323
+ if (!grow)
324
+ return data.tree;
325
+ const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
326
+ if (prevLen > 0)
331
327
  data.boundaries.push(prevLen);
332
- const { tree, pyramid } = bytesToTreePyramid(this.space, this.alphabet, grown, data.pyramid);
333
- data.pyramid = pyramid;
328
+ const tree = bytesToTree(this.space, this.alphabet, grown, undefined, undefined, data.boundaries.length > 0 ? data.boundaries : undefined);
329
+ data.tree = tree;
334
330
  data.bytes = grown;
335
331
  data.perceiveMemo.set(latin1Key(grown), tree);
336
332
  return tree;
@@ -377,6 +373,15 @@ export class Mind {
377
373
  this.canon = this._canonFor(typeof turn === "string" ? textCanon : null);
378
374
  this.canonMemo = this.canon ? new Map() : null;
379
375
  try {
376
+ // No recognise-memo pre-seeding here: that used to be necessary
377
+ // because the flat/positional fold lost visibility into an earlier
378
+ // turn's own structure once later bytes shifted its position
379
+ // (foldTree no longer visited the turn's root node). The
380
+ // STABLE-PREFIX fold (see {@link ConversationData}) makes every
381
+ // turn's subtree independent of what follows it by construction, so
382
+ // recognise() finds it correctly on its own, first-touch, exactly
383
+ // once per turn — no workaround needed, and none pre-empting
384
+ // recognise()'s own memo with a partial result.
380
385
  const top = this.trace?.enter("respondTurn", [
381
386
  rItem(newContext, "query"),
382
387
  ]);
@@ -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. */