@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.
@@ -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;
@@ -6,7 +6,25 @@ import type { Alphabet } from "../alphabet.js";
6
6
  import type { MindConfig } from "../config.js";
7
7
  import type { GraphSearch, Leaf, Seg, Site } from "./graph-search.js";
8
8
  import type { Rationale } from "./rationale.js";
9
- import type { FoldPyramid, Grid } from "../geometry.js";
9
+ import type { FoldPyramid, Grid, StableFold } from "../geometry.js";
10
+ /** One {@link MindContext._depositTrees} entry — see that field's doc. */
11
+ export interface DepositCacheEntry {
12
+ /** Turn boundaries accumulated over this content's deposit chain —
13
+ * strictly increasing proper offsets, each a previously-deposited
14
+ * whole-context length. Empty for a first-seen (single-turn) input. */
15
+ boundaries: number[];
16
+ /** Plain-fold pyramid (first-seen inputs only). */
17
+ pyramid?: FoldPyramid;
18
+ /** Stable-prefix segment folds (grown-context inputs only). */
19
+ stable?: StableFold;
20
+ /** The continuation bytes this ctxInput was paired with in ingestPair, if
21
+ * any — the ONLY thing that makes a later, longer ctxInput a genuine next
22
+ * TURN of the same conversation rather than an unrelated fact that
23
+ * happens to share this one's byte prefix (e.g. "2+2" vs. "2+2=5"). A
24
+ * later deposit only takes this entry as its stable-prefix `prev` when
25
+ * its own suffix bytes-equal this exactly. */
26
+ nextBytes?: Uint8Array;
27
+ }
10
28
  export type Input = string | Uint8Array | Grid | Grid[];
11
29
  /** The host capabilities GraphSearch consults during a cover. MindContext
12
30
  * extends this so the Mind can pass itself as the host. */
@@ -153,15 +171,19 @@ export interface MindContext extends GraphSearchHost {
153
171
  * never invalidated. Bounded LRU (byte-sized); a miss only re-perceives,
154
172
  * never a correctness risk. */
155
173
  _gistCache: BoundedMap<number, Vec>;
156
- /** DEPOSIT-path stable-prefix tree cache: content key (latin1) of a
157
- * deposited input → its plain-fold level PYRAMID, so the NEXT deposit
158
- * whose prefix it is (a conversation's accumulated context) reuses every
159
- * full aligned block and refolds only the right edge of each level
160
- * O(turn) per deposit instead of O(context), with the tree BIT-IDENTICAL
161
- * to a from-scratch fold (see FoldPyramid). Purely a performance cache.
162
- * Entry-count bounded (a pyramid is ~KB/byte of content; only the few
163
- * live conversation chains matter). */
164
- _depositTrees: BoundedMap<string, FoldPyramid>;
174
+ /** DEPOSIT-path perception cache: content key (latin1) of a deposited
175
+ * input → its accumulated turn BOUNDARIES plus reusable fold state. A
176
+ * deposit whose content extends a cached entry IS a conversation context
177
+ * grown by one turn the cached length is the new boundary so it
178
+ * folds with the SAME stable-prefix fold query-time perception uses
179
+ * (structural train/inference agreement, load-bearing for recall),
180
+ * reusing every already-folded segment via `stable` (see StableFold)
181
+ * O(turn) per deposit instead of O(context). A first-seen input keeps
182
+ * the plain fold and caches its `pyramid` (see FoldPyramid). Purely a
183
+ * performance cache for the FOLD STATE; the boundaries are semantic but
184
+ * derived only from the deposit sequence itself (an evicted chain falls
185
+ * back to plain-fold behavior, exactly the pre-boundary shape). */
186
+ _depositTrees: BoundedMap<string, DepositCacheEntry>;
165
187
  /** The byte lengths present in {@link _depositTrees} — the candidate
166
188
  * prefix lengths probed (longest first). Drifts on eviction (a stale
167
189
  * length only costs a miss); cleared with the map when it outgrows the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
package/src/geometry.ts CHANGED
@@ -145,7 +145,7 @@ export function coverageBar(_maxGroup: number, D: number): number {
145
145
 
146
146
  // ---- types ----
147
147
 
148
- interface Folded {
148
+ export interface Folded {
149
149
  tree: Sema;
150
150
  /** Byte length of the subtree — carried incrementally so the stable-prefix
151
151
  * boundary scan never re-walks subtrees (the old per-level walk was
@@ -383,6 +383,66 @@ function stablePrefixFold(
383
383
  return cur.tree;
384
384
  }
385
385
 
386
+ /** A stable-prefix fold's reusable state: the segment edge offsets and each
387
+ * segment's independently-folded root ({@link riverFoldRaw} output). A
388
+ * grown stream whose boundary set EXTENDS a previous fold's reuses every
389
+ * matching segment's Folded unchanged (segments fold independently by
390
+ * construction, so reuse is bit-identical to refolding) and folds only the
391
+ * new right-edge segment — O(turn) per extension, the stable-prefix
392
+ * counterpart of {@link FoldPyramid}. Purely a cache: the produced tree
393
+ * never depends on cache state. */
394
+ export interface StableFold {
395
+ edges: number[];
396
+ segs: Folded[];
397
+ }
398
+
399
+ /** {@link stablePrefixFold} with incremental segment reuse — same cuts, same
400
+ * segment folds, same left-nested join, same single root normalize; `prev`
401
+ * only elides recomputing segments whose [start,end) offsets it already
402
+ * folded over a byte-identical prefix (the caller keys the cache by
403
+ * content). Requires a non-empty effective boundary set. */
404
+ export function stablePrefixFoldIncremental(
405
+ space: Space,
406
+ alphabet: Alphabet,
407
+ bytes: Uint8Array,
408
+ boundaries: readonly number[],
409
+ prev?: StableFold,
410
+ ): { tree: Sema; fold: StableFold } {
411
+ const cuts: number[] = [];
412
+ let prevB = 0;
413
+ for (const b of boundaries) {
414
+ if (b > prevB && b < bytes.length) {
415
+ cuts.push(b);
416
+ prevB = b;
417
+ }
418
+ }
419
+ const edges = [0, ...cuts, bytes.length];
420
+ const segs: Folded[] = [];
421
+ for (let i = 0; i + 1 < edges.length; i++) {
422
+ const hit = prev !== undefined && prev.edges[i] === edges[i] &&
423
+ prev.edges[i + 1] === edges[i + 1]
424
+ ? prev.segs[i]
425
+ : undefined;
426
+ segs.push(
427
+ hit ??
428
+ riverFoldRaw(
429
+ space,
430
+ bytesToLeaves(alphabet, bytes.subarray(edges[i], edges[i + 1])),
431
+ ),
432
+ );
433
+ }
434
+ if (segs.length === 1) {
435
+ // Degenerate boundary set — the plain fold, as stablePrefixFold does.
436
+ const tree =
437
+ riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
438
+ return { tree, fold: { edges, segs } };
439
+ }
440
+ let cur = segs[0];
441
+ for (let i = 1; i < segs.length; i++) cur = fold2(space, cur, segs[i]);
442
+ normalize(cur.tree.v);
443
+ return { tree: cur.tree, fold: { edges, segs } };
444
+ }
445
+
386
446
  /** Join two folded items as one 2-kid branch — the top-level join of the
387
447
  * stable-prefix fold, identical FP ops to foldSlice's seat-bound
388
448
  * accumulation over a group of two. Unnormalized (interior). */
@@ -867,6 +867,28 @@ async function crossRegionVotes(
867
867
  const rb = regions[cand[b]];
868
868
  if (!strong.has(cand[a]) && !strong.has(cand[b])) continue;
869
869
  if (ra.end >= rb.start) continue; // overlap or adjacent — nothing between
870
+ // Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
871
+ // that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
872
+ // genuine, individually-corroborated evidence about what fills the gap
873
+ // — gate the container search below: a joint container is binding
874
+ // evidence only when it is CONSISTENT with that evidence, i.e. its own
875
+ // bytes actually contain what the between-region says. This is the
876
+ // n-ary composition's normal shape (a between-attribute's bytes DO
877
+ // recur inside the joint container, credited as an "extra" below) as
878
+ // opposed to a container that silently substitutes something else for
879
+ // it (e.g. bridging past "Italy" to a container whose interior is
880
+ // "Japan" — a different, contradicting learnt whole).
881
+ // Only a KNOWN (content-addressed, exact) between-region qualifies —
882
+ // an approximate region's resonance climbing "somewhere" is ordinary
883
+ // noise (any ANN query returns SOME nearest neighbour), not evidence
884
+ // this specific gap already means something specific.
885
+ const between: number[] = [];
886
+ for (let m = a + 1; m < b; m++) {
887
+ if (
888
+ strong.has(cand[m]) && !consumed.has(cand[m]) &&
889
+ regions[cand[m]].known
890
+ ) between.push(cand[m]);
891
+ }
870
892
  // A single KNOWN region covering both: the whole form is already a
871
893
  // stored identity that votes directly; its pieces add nothing.
872
894
  if (
@@ -932,6 +954,22 @@ async function crossRegionVotes(
932
954
  );
933
955
  if (indexOf(query, joined, 0) >= 0) continue; // query says it itself
934
956
  }
957
+ // CONTRADICTION GUARD: a between-region already carrying its own
958
+ // vote must actually recur in this container's bytes — otherwise
959
+ // the container is a different learnt whole that happens to share
960
+ // ra/rb, and letting it stand in for the gap would silently
961
+ // override evidence the query itself already resolved there.
962
+ if (
963
+ between.some((bi) =>
964
+ indexOf(
965
+ bytes,
966
+ query.subarray(regions[bi].start, regions[bi].end),
967
+ 0,
968
+ ) < 0
969
+ )
970
+ ) {
971
+ continue;
972
+ }
935
973
  let cov = left.length + right.length;
936
974
  const extras: number[] = [];
937
975
  for (const ei of cand) {
@@ -707,52 +707,104 @@ export class GraphSearch {
707
707
  return;
708
708
  }
709
709
 
710
- // LIMIT 2 decides all three facts this rule needs — emptiness, plurality
711
- // (whether to consult the disambiguator), and the first-inserted
712
- // fallback without materialising a hub context's corpus-sized fan-out.
713
- const nx = this.store.nextFirst(it.node, 2);
714
- if (nx.length) {
715
- // A direct edge step along the chain toward its fixpoint. A recomposed
716
- // form (parts already rewritten and fused into a learned whole) follows
717
- // its continuation at MICRO, so reaching the grounded answer of the
718
- // recomposition beats leaving the parts split; the flag rides the chain so
719
- // every step of the recomposition's completion stays cheap.
720
- //
721
- // WHICH edge: a context node often carries SEVERAL learnt continuations
722
- // (the same sentence trained against 100+ target languages, a question
723
- // answered differently across sessions). `nx[0]` is an accident of
724
- // training order; when the host provides a `chooseNext` disambiguator
725
- // (see Mind.chooseNext) it picks the continuation with the most
726
- // distributional evidence (prevOf count — the structural manifestation
727
- // of its halo), falling back to first-inserted when evidence is equal.
728
- yield {
729
- premises: [it],
730
- conclusion: {
731
- kind: "form",
732
- i: it.i,
733
- j: it.j,
734
- node: (nx.length > 1 ? this.host.chooseNext?.(it.node) : undefined) ??
735
- nx[0],
736
- via: true,
737
- rcmp: it.rcmp,
738
- },
739
- // A recomposed form's continuation is FREE: the two (or more) parts were
740
- // already paid for as their own rewrites, and the single consolidated
741
- // span saves one cover-bridge versus leaving them split — so charging 0
742
- // here makes the grounded whole (e.g. "DE"→F) strictly beat the split
743
- // ("D","E") by exactly that saved bridge, deterministically.
744
- cost: it.rcmp ? 0 : STEP,
745
- };
746
- } else if (it.via) {
747
- // The chain reached a node with no WHOLE-node continuation. Before
748
- // emitting it as terminal, CONTINUE THE EXPLORATION into its own structure:
749
- // a composite answer like "p1 p2" leads nowhere as a whole, yet recognising
750
- // it surfaces p1, p2 each of which continues (→ R1, R2) and recomposes
751
- // into a deeper learnt form (→ FINAL). {@link recompleteNode} re-covers the
752
- // node's bytes through the SAME recognition + edge/fuse machinery the top
753
- // query uses (a continued graph exploration, not a re-perception), and
754
- // returns the deeper completion's bytes when it genuinely leads somewhere
755
- // new. Emit that; else emit the node itself as the terminal answer.
710
+ if (it.via) {
711
+ // A CHAIN hop (this form was itself reached by following an edge):
712
+ // read up to the hub bound the SAME √(max(2,N)) convention {@link
713
+ // hubBound} in traverse.ts derives, replicated here because
714
+ // GraphSearch holds only a bare Store, not a MindContext — and fork
715
+ // across EVERY continuation. A bidirectional pair (SmolSent trains
716
+ // both directions) puts the back-edge to where we came from ahead of
717
+ // the forward edge in insertion order; a single first-inserted pick
718
+ // would walk the search straight into a cycle (the A*LD duplicate-key
719
+ // guard then dead-ends it) with no way to reach the forward edge.
720
+ // Forking offers every continuation as its own rule so the one that
721
+ // genuinely advances (not a duplicate) is still reachable.
722
+ const bound = Math.ceil(
723
+ Math.sqrt(Math.max(2, this.store.edgeSourceCount())),
724
+ );
725
+ const nx = this.store.nextFirst(it.node, bound);
726
+ if (nx.length) {
727
+ // The SAME evidence-weighted disambiguation the first hop uses
728
+ // (below) identifies the most-corroborated continuation. Yielding
729
+ // it FIRST matters: {@link lightestDerivation}'s chart keeps the
730
+ // FIRST rule to reach a given cost for an item and discards later
731
+ // arrivals at an EQUAL cost (`cost < current`, strictly) — so
732
+ // among same-depth sibling forks that tie in cost, the
733
+ // evidence-backed edge wins deterministically, never by
734
+ // exploration-order luck. `preferred`, when set, is necessarily
735
+ // an element of `nx` (chooseNext reads the identical hub-bounded
736
+ // set — see traverse.ts), so a plain skip-in-place suffices; no
737
+ // second array need be allocated to reorder it to the front.
738
+ const preferred = nx.length > 1
739
+ ? this.host.chooseNext?.(it.node)
740
+ : undefined;
741
+ const fork = (node: number): Rule<GItem> => ({
742
+ premises: [it],
743
+ conclusion: { kind: "form", i: it.i, j: it.j, node, via: true },
744
+ // A real edge always costs STEP, whether it is the first hop or
745
+ // the fifth — so a chain's total cost is proportional to its
746
+ // length and the lightest derivation is always the SHORTEST
747
+ // successful one. (Charging every hop after the first for
748
+ // FREE, as an earlier design did, made every stopping point at
749
+ // any depth tie at the same cost indistinguishable to the
750
+ // search, decided by whichever arrived first.)
751
+ cost: STEP,
752
+ });
753
+ if (preferred !== undefined) yield fork(preferred);
754
+ for (const c of nx) {
755
+ if (c !== preferred) yield fork(c);
756
+ }
757
+ // Also offer stopping HERE, at CONCEPT above whatever the chain
758
+ // has already cost — the same "a synonym jump is dearer than a
759
+ // direct edge" ordering the ladder already uses elsewhere,
760
+ // repurposed as "giving up early is dearer than one more real
761
+ // hop". A premature stop at depth D costs D·STEP + CONCEPT, so a
762
+ // SHORTER premature stop always beats a longer one, and a genuine
763
+ // fixpoint (below, costing +0) always beats any premature stop at
764
+ // the same depth — the search only settles here when continuing
765
+ // genuinely dead-ends (every fork above revisits an already-
766
+ // explored key) or grows costlier than giving up.
767
+ //
768
+ // The stop-here bytes are the node's OWN, as-is — NOT recompleteNode's
769
+ // recursive re-cover. recompleteNode re-runs the full recognition
770
+ // + edge/fuse search over the node's bytes, an O(node's own
771
+ // substructure) cost independent of chain depth; offering it at
772
+ // EVERY premature stop makes a query's total cost scale with how
773
+ // many nodes the corpus happens to interconnect here (corpus
774
+ // density), not with the answer's own hop count — the exact
775
+ // output-sensitivity the rest of this search is built to keep.
776
+ // Reserved below for the ONE place it is load-bearing: the true
777
+ // fixpoint, checked once, at the chain's actual end.
778
+ yield {
779
+ premises: [it],
780
+ conclusion: {
781
+ kind: "out",
782
+ i: it.i,
783
+ j: it.j,
784
+ bytes: nodeBytes(it.node),
785
+ cover: true,
786
+ rec: true,
787
+ node: it.node,
788
+ },
789
+ cost: CONCEPT,
790
+ };
791
+ return;
792
+ }
793
+ // The chain reached a node with no WHOLE-node continuation anywhere
794
+ // — a genuine fixpoint, not a premature stop. Before emitting it as
795
+ // terminal, CONTINUE THE EXPLORATION into its own structure: a
796
+ // composite answer like "p1 p2" leads nowhere as a whole, yet
797
+ // recognising it surfaces p1, p2 — each of which continues (→ R1,
798
+ // R2) and recomposes into a deeper learnt form (→ FINAL). {@link
799
+ // recompleteNode} re-covers the node's bytes through the SAME
800
+ // recognition + edge/fuse machinery the top query uses (a continued
801
+ // graph exploration, not a re-perception), and returns the deeper
802
+ // completion's bytes when it genuinely leads somewhere new. Emit
803
+ // that; else emit the node itself as the terminal answer. This is
804
+ // the ONE place the recursive re-cover runs — once, at the chain's
805
+ // actual end, never per intermediate stop — so its cost tracks the
806
+ // ANSWER's own structure, not how densely the corpus interconnects
807
+ // the nodes passed through on the way there.
756
808
  const deeper = this.recompleteNode(it.node);
757
809
  yield {
758
810
  premises: [it],
@@ -768,6 +820,36 @@ export class GraphSearch {
768
820
  cost: 0,
769
821
  };
770
822
  } else {
823
+ // The FIRST hop out of a recognised site — unchanged from the
824
+ // original design. LIMIT 2 only senses PLURALITY (does this node
825
+ // have more than one learnt continuation?); the actual bounded read
826
+ // and evidence-weighted pick both live inside {@link
827
+ // Mind.chooseNext} (see traverse.ts), which floors its own hub bound
828
+ // at √(max(2,N)) regardless of what we read here.
829
+ const nx = this.store.nextFirst(it.node, 2);
830
+ if (nx.length) {
831
+ yield {
832
+ premises: [it],
833
+ conclusion: {
834
+ kind: "form",
835
+ i: it.i,
836
+ j: it.j,
837
+ node:
838
+ (nx.length > 1 ? this.host.chooseNext?.(it.node) : undefined) ??
839
+ nx[0],
840
+ via: true,
841
+ rcmp: it.rcmp,
842
+ },
843
+ // A recomposed form's continuation is FREE: the two (or more) parts
844
+ // were already paid for as their own rewrites, and the single
845
+ // consolidated span saves one cover-bridge versus leaving them
846
+ // split — so charging 0 here makes the grounded whole (e.g.
847
+ // "DE"→F) strictly beat the split ("D","E") by exactly that saved
848
+ // bridge, deterministically.
849
+ cost: it.rcmp ? 0 : STEP,
850
+ };
851
+ return;
852
+ }
771
853
  // Recognised but edge-less: borrow a concept (halo) sibling's edge. No
772
854
  // edge and no concept means the form leads nowhere — it yields no rule, so
773
855
  // a query of only such forms produces no derivation, and think is silent.
@@ -9,6 +9,7 @@ import type { Input, MindContext } from "./types.js";
9
9
  import { changedNodes } from "./types.js";
10
10
  import {
11
11
  inputBytes,
12
+ latin1Key,
12
13
  perceive,
13
14
  perceiveDeposit,
14
15
  resolve,
@@ -119,6 +120,7 @@ export async function deposit(
119
120
  ctx: MindContext,
120
121
  input: Input,
121
122
  track: boolean,
123
+ conversational = false,
122
124
  ): Promise<
123
125
  { tree: Sema; rootId: number; ids: Map<Sema, number>; changed: Sema[] }
124
126
  > {
@@ -129,8 +131,13 @@ export async function deposit(
129
131
  // (no store-probe fallback): a knownPrefixLength scan on every novel fact
130
132
  // would cost O(n²) hashing, while conversation replays are always warm —
131
133
  // re-deposition replays from the first turn, rebuilding the cache as it
132
- // goes.
133
- const tree = perceiveDeposit(ctx, bytes);
134
+ // goes. `conversational` scopes the STABLE-PREFIX variant (turn-boundary
135
+ // folding, matching query-time perception) to ingestPair's own growing
136
+ // context argument — a bare ingestOne deposit whose bytes merely happen
137
+ // to extend an earlier UNRELATED deposit (no conversational relationship)
138
+ // must keep the plain fold, or two coincidentally-prefix-sharing facts
139
+ // would stop sharing structure with each other.
140
+ const tree = perceiveDeposit(ctx, bytes, conversational);
134
141
 
135
142
  const ids = new Map<Sema, number>();
136
143
  const rootId = await internTreeIds(ctx, tree, ids);
@@ -214,10 +221,20 @@ export async function ingestPair(
214
221
  ctxInput: Input,
215
222
  cont: Input,
216
223
  ): Promise<void> {
217
- const c = await deposit(ctx, ctxInput, true);
224
+ const c = await deposit(ctx, ctxInput, true, true);
218
225
  const cont_ = await deposit(ctx, cont, false);
219
226
  const ctxId = c.rootId, contId = cont_.rootId;
220
227
 
228
+ // Stamp this turn's continuation onto its own cache entry — the proof a
229
+ // FUTURE, longer ctxInput needs (see perceiveDeposit) to recognise itself
230
+ // as this conversation's genuine next turn rather than an unrelated fact
231
+ // that happens to share this ctxInput's byte prefix.
232
+ {
233
+ const ctxBytes = inputBytes(ctx, ctxInput);
234
+ const entry = ctx._depositTrees.get(latin1Key(ctxBytes));
235
+ if (entry !== undefined) entry.nextBytes = inputBytes(ctx, cont);
236
+ }
237
+
221
238
  await ctx.store.link(ctxId, contId);
222
239
  await propagateSuffixes(ctx, ctxId, contId);
223
240
 
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { MindContext } from "../types.js";
13
13
  import type { ComputedResult, Site } from "../graph-search.js";
14
+ import { bytesEqual, indexOf } from "../../bytes.js";
14
15
  import { read, resolve } from "../primitives.js";
15
16
  import { guidedFirst } from "../traverse.js";
16
17
  import { conceptHop } from "../match.js";
@@ -217,6 +218,43 @@ export const coverMechanism: PipelineMechanism = {
217
218
 
218
219
  if (segs === null) return [];
219
220
 
221
+ // A chosen span's SUBSTITUTED bytes (an edge followed from a recognised
222
+ // site, not the site's own literal text read back) that equal a byte
223
+ // span the query ALREADY CONTAINS elsewhere restates part of the
224
+ // question — never an answer (the same principle recall.ts's tiers
225
+ // apply to a whole-query projection: "a projection that is a proper
226
+ // byte-subspan of the query restates part of the question"). A
227
+ // recognised site that is itself an entire PRIOR TURN of a multi-turn
228
+ // query is exactly this shape: it carries a genuine learnt
229
+ // continuation, but that continuation is something the asker already
230
+ // said moments later in the SAME query, not a new answer — the cover
231
+ // search has no notion of "turn" to gate this itself, so the check
232
+ // belongs here, over the derivation it already chose.
233
+ const W = ctx.space.maxGroup;
234
+ for (const s of segs) {
235
+ if (!s.rec) continue;
236
+ const literal = s.j - s.i === s.bytes.length &&
237
+ bytesEqual(s.bytes, query.subarray(s.i, s.j));
238
+ if (literal) continue;
239
+ // A span shorter than one river window is exactly the "ice"/"c"
240
+ // case: a chain hop's short terminal coincidentally recurring as a
241
+ // substring of an unrelated longer word. Below the quantum, byte
242
+ // overlap is chance, not evidence — the same floor identityBar and
243
+ // reachThreshold hold every other structural-overlap claim to.
244
+ if (
245
+ s.bytes.length >= W && s.bytes.length < query.length &&
246
+ indexOf(query, s.bytes, 0) >= 0
247
+ ) {
248
+ ctx.trace?.step(
249
+ "restatedSpan",
250
+ [rItem(s.bytes, "substituted", s.node, [s.i, s.j])],
251
+ [],
252
+ "the chosen span's substitution already occurs elsewhere in the query — restates it, not an answer",
253
+ );
254
+ return [];
255
+ }
256
+ }
257
+
220
258
  const composed = liftAnswer(segs, query.length);
221
259
  if (composed === null) return [];
222
260
 
@@ -113,10 +113,38 @@ export async function recallByResonance(
113
113
  maximal.push(s);
114
114
  maxEnd = s.end;
115
115
  }
116
- if (maximal.length === 1) {
116
+ // The wrapper must actually BE scaffolding: RC8's own premise is "the
117
+ // wrapper is scaffolding; the argument is the span that leads
118
+ // somewhere" ("How do you say 'thank you' in French?" — everything
119
+ // outside the argument is a small fixed template). When the query
120
+ // instead has ANOTHER substantial recognised form (≥ W2, the same
121
+ // constituent bar the argument itself must clear) sitting OUTSIDE the
122
+ // chosen argument, the query is not one argument in a wrapper — it is
123
+ // several complete, independently-meaningful pieces (a multi-turn
124
+ // conversation's own accumulated turns are exactly this shape), and
125
+ // binding to the argument's continuation would answer past content
126
+ // the query itself already carries forward. Derived from the same W2
127
+ // bar the argument itself is held to, never a separate tuned number.
128
+ const hasSubstantialOutside = maximal.length === 1 &&
129
+ pre.rec.sites.some((s) =>
130
+ s.end - s.start >= W2 &&
131
+ (s.end <= maximal[0].start || s.start >= maximal[0].end)
132
+ );
133
+ if (maximal.length === 1 && !hasSubstantialOutside) {
117
134
  const arg = maximal[0];
118
135
  const g = await follow(ctx, arg.payload, queryGist);
119
- if (g !== null && g.length > 0) {
136
+ // The same "no restated fragment" guard tier 2 applies below (§ "the
137
+ // anchor cleared the consensus floor..."): a followed continuation
138
+ // that is itself a proper byte-subspan of the QUERY restates part of
139
+ // the question — never an answer. A multi-turn query's own later
140
+ // turns are exact, content-addressed matches for exactly this reason
141
+ // (each turn is its own previously-learnt form), so without this
142
+ // guard the argument's OWN later restatement in the same
143
+ // conversation reads as if it were the next thing to say.
144
+ if (
145
+ g !== null && g.length > 0 &&
146
+ !(g.length < query.length && indexOf(query, g, 0) >= 0)
147
+ ) {
120
148
  return ground(
121
149
  g,
122
150
  "argument binding — the query's sole edge-source constituent, continuation followed",