@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
@@ -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). */
@@ -83,7 +83,7 @@ export async function articulate(ctx, answer, query) {
83
83
  s.end,
84
84
  ])),
85
85
  ]);
86
- const solved = ctx.search.cover(answer.length, voicedSites, new Map(), ans.leaves, ans.splits, substitutions, undefined, undefined, ctx.trace ? (steps) => traceDerivation(ctx, steps) : undefined);
86
+ const solved = ctx.search.cover(answer.length, voicedSites, new Map(), ans.leaves, ans.splits, ans.starts, substitutions, undefined, undefined, ctx.trace ? (steps) => traceDerivation(ctx, steps) : undefined);
87
87
  const segs = solved && solved.segs;
88
88
  tArtCover?.done(segs === null
89
89
  ? []
@@ -39,6 +39,9 @@ export declare function poolVotes(ctx: MindContext, regionVotes: readonly Region
39
39
  end: number;
40
40
  w: number;
41
41
  }>;
42
+ /** Per-anchor SCALE-INVARIANT support: Σ RegionVote.absorbed over the
43
+ * distinct contributing regions — see Attention.breadth. */
44
+ regionSupport: Map<number, number>;
42
45
  steps: DerivationStep[];
43
46
  };
44
47
  export declare function commitVotes(ctx: MindContext, pooled: {
@@ -49,6 +52,7 @@ export declare function commitVotes(ctx: MindContext, pooled: {
49
52
  end: number;
50
53
  w: number;
51
54
  }>;
55
+ regionSupport: Map<number, number>;
52
56
  steps: DerivationStep[];
53
57
  }, sat: SaturationInfo, regions: readonly Region[], regionVoter: ReadonlyArray<{
54
58
  id: number;
@@ -400,6 +400,7 @@ export function poolVotes(ctx, regionVotes, sat, N) {
400
400
  const votes = new Map();
401
401
  const votesIdf = new Map();
402
402
  const support = new Map();
403
+ const regionSupport = new Map();
403
404
  const steps = [];
404
405
  let order = 0;
405
406
  for (const pc of pool.values()) {
@@ -407,14 +408,17 @@ export function poolVotes(ctx, regionVotes, sat, N) {
407
408
  votes.set(pc.item.id, pc.cost);
408
409
  const premises = [];
409
410
  const seenRi = new Set();
411
+ let breadthSum = 0;
410
412
  for (const c of pc.contributions) {
411
413
  const p0 = c.premises[0].item;
412
414
  if (p0.kind !== "region" || seenRi.has(p0.ri))
413
415
  continue;
414
416
  seenRi.add(p0.ri);
415
417
  const rv = regionVotes[p0.ri];
418
+ breadthSum += rv.absorbed ?? 1;
416
419
  premises.push({ kind: "form", span: [rv.start, rv.end] });
417
420
  }
421
+ regionSupport.set(pc.item.id, breadthSum);
418
422
  steps.push({
419
423
  order: order++,
420
424
  move: "pool-vote",
@@ -444,18 +448,29 @@ export function poolVotes(ctx, regionVotes, sat, N) {
444
448
  }
445
449
  }
446
450
  }
447
- return { votes, votesIdf, support, steps };
451
+ return { votes, votesIdf, support, regionSupport, steps };
448
452
  }
449
453
  export function commitVotes(ctx, pooled, sat, regions, regionVoter, N) {
450
- const { votes, votesIdf, support, steps } = pooled;
454
+ const { votes, votesIdf, support, regionSupport, steps } = pooled;
451
455
  if (votes.size === 0) {
452
456
  traceAttention(ctx, regions, regionVoter, [], steps);
453
457
  return { roots: [], ranked: [] };
454
458
  }
459
+ // SCALE-INVARIANT confidence — see Attention.breadth's doc. regions.length
460
+ // is the query's OWN full candidate count (most never vote at all), the
461
+ // same denominator the "N of M sub-regions voted" rationale text already
462
+ // reports; regionSupport is that same accounting read PER ANCHOR.
463
+ const totalRegions = Math.max(1, regions.length);
455
464
  const ranked = [...votes.entries()]
456
465
  .map(([anchor, vote]) => {
457
466
  const s = support.get(anchor);
458
- return { anchor, vote, start: s.start, end: s.end };
467
+ return {
468
+ anchor,
469
+ vote,
470
+ start: s.start,
471
+ end: s.end,
472
+ breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
473
+ };
459
474
  })
460
475
  .sort((a, b) => b.vote - a.vote);
461
476
  const overlaps = (a, b) => a.start < b.end && b.start < a.end;
@@ -736,6 +751,27 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
736
751
  continue;
737
752
  if (ra.end >= rb.start)
738
753
  continue; // overlap or adjacent — nothing between
754
+ // Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
755
+ // that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
756
+ // genuine, individually-corroborated evidence about what fills the gap
757
+ // — gate the container search below: a joint container is binding
758
+ // evidence only when it is CONSISTENT with that evidence, i.e. its own
759
+ // bytes actually contain what the between-region says. This is the
760
+ // n-ary composition's normal shape (a between-attribute's bytes DO
761
+ // recur inside the joint container, credited as an "extra" below) as
762
+ // opposed to a container that silently substitutes something else for
763
+ // it (e.g. bridging past "Italy" to a container whose interior is
764
+ // "Japan" — a different, contradicting learnt whole).
765
+ // Only a KNOWN (content-addressed, exact) between-region qualifies —
766
+ // an approximate region's resonance climbing "somewhere" is ordinary
767
+ // noise (any ANN query returns SOME nearest neighbour), not evidence
768
+ // this specific gap already means something specific.
769
+ const between = [];
770
+ for (let m = a + 1; m < b; m++) {
771
+ if (strong.has(cand[m]) && !consumed.has(cand[m]) &&
772
+ regions[cand[m]].known)
773
+ between.push(cand[m]);
774
+ }
739
775
  // A single KNOWN region covering both: the whole form is already a
740
776
  // stored identity that votes directly; its pieces add nothing.
741
777
  if (regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end))
@@ -781,6 +817,14 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
781
817
  if (indexOf(query, joined, 0) >= 0)
782
818
  continue; // query says it itself
783
819
  }
820
+ // CONTRADICTION GUARD: a between-region already carrying its own
821
+ // vote must actually recur in this container's bytes — otherwise
822
+ // the container is a different learnt whole that happens to share
823
+ // ra/rb, and letting it stand in for the gap would silently
824
+ // override evidence the query itself already resolved there.
825
+ if (between.some((bi) => indexOf(bytes, query.subarray(regions[bi].start, regions[bi].end), 0) < 0)) {
826
+ continue;
827
+ }
784
828
  let cov = left.length + right.length;
785
829
  const extras = [];
786
830
  for (const ei of cand) {
@@ -826,14 +870,6 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
826
870
  spanStart = Math.min(spanStart, regions[ei].start);
827
871
  spanEnd = Math.max(spanEnd, regions[ei].end);
828
872
  }
829
- out.push({
830
- start: spanStart,
831
- end: spanEnd,
832
- canonicalFailed: false, // content-addressed: never saturation-masked
833
- roots: reach.roots,
834
- w,
835
- wFocus: w,
836
- });
837
873
  consumed.add(cand[a]);
838
874
  consumed.add(cand[b]);
839
875
  for (const ei of bestExtras)
@@ -843,15 +879,32 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
843
879
  // vote's bytes are literally part of the learnt whole), and FULL root
844
880
  // disjointness is the disagreement test: a vote sharing even one root
845
881
  // with the junction corroborates it and keeps its say elsewhere.
882
+ // Counted BEFORE pushing the junction's own vote below: each ORIGINAL
883
+ // region this ascent explains away is evidence the junction speaks
884
+ // for, not evidence lost — `absorbed` (RegionVote's breadth-accounting
885
+ // field) must credit the junction with all of it, not just the ONE
886
+ // pooled axiom it collapses to.
846
887
  const containerBytes = cachedRead(ctx, cache, best.id, cap);
847
888
  const jointRoots = new Set(reach.roots);
889
+ let explainedAway = 0;
848
890
  for (const rv of rvs.votes) {
849
891
  if (rv.roots.some((r) => jointRoots.has(r)))
850
892
  continue;
851
893
  const bytes = query.subarray(rv.start, rv.end);
852
- if (indexOf(containerBytes, bytes, 0) >= 0)
894
+ if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
853
895
  superseded.add(rv);
896
+ explainedAway++;
897
+ }
854
898
  }
899
+ out.push({
900
+ start: spanStart,
901
+ end: spanEnd,
902
+ canonicalFailed: false, // content-addressed: never saturation-masked
903
+ roots: reach.roots,
904
+ w,
905
+ wFocus: w,
906
+ absorbed: 1 + explainedAway,
907
+ });
855
908
  const label = [cand[a], cand[b], ...bestExtras]
856
909
  .sort((x, y) => regions[x].start - regions[y].start)
857
910
  .map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
@@ -54,6 +54,18 @@ export type GItem = {
54
54
  cover: boolean;
55
55
  rec: boolean;
56
56
  node?: number;
57
+ /** Set only for a {@link ComputedResult} axiom (an extension's derived
58
+ * value, e.g. ALU arithmetic) — as opposed to a genuinely RECOGNISED
59
+ * learned form. Both set `rec: true` (a computation bridges the cover
60
+ * exactly like a learned terminal answer), but they mean different
61
+ * things for `i..j`'s WIDTH: a recognised form's query-span width is
62
+ * real evidence of how much of the query's meaning it accounts for
63
+ * (liftAnswer's half-dominance framing decision); a computed span's
64
+ * width is operand digit-count, uncorrelated with meaning ("1000 - 421"
65
+ * is wider than "15 * 7" only because the numbers are bigger, not
66
+ * because subtraction is more "the point" of its query). See
67
+ * {@link liftAnswer}. */
68
+ computed?: boolean;
57
69
  };
58
70
  export declare const STEP = 1;
59
71
  export declare const CONCEPT = 10;
@@ -72,6 +84,9 @@ export interface Seg {
72
84
  bytes: Uint8Array;
73
85
  rec: boolean;
74
86
  node?: number;
87
+ /** See the `computed` field of the "out" {@link GItem} — set only for an
88
+ * extension's derived value, never a genuinely recognised learned form. */
89
+ computed?: boolean;
75
90
  }
76
91
  /** One rule application inside the cover's lightest derivation — the FINEST
77
92
  * grain of Sema's core reasoning, one node of the adapted A*LD proof tree. `move`
@@ -144,7 +159,7 @@ export declare class GraphSearch {
144
159
  * Any learnt connector between two rewrites is spliced IN by the in-search
145
160
  * connector rule (see {@link outRules}), so the returned spans already carry
146
161
  * it — there is no post-pass. */
147
- cover(queryLen: number, sites: ReadonlyArray<Site>, conceptTarget: ReadonlyMap<number, number>, leaves: ReadonlyArray<Leaf>, splits: ReadonlySet<number>, substitutions?: ReadonlyMap<number, Uint8Array>, connectors?: ReadonlyMap<string, Uint8Array>, computedResults?: ReadonlyArray<ComputedResult>,
162
+ cover(queryLen: number, sites: ReadonlyArray<Site>, conceptTarget: ReadonlyMap<number, number>, leaves: ReadonlyArray<Leaf>, splits: ReadonlySet<number>, starts: ReadonlySet<number>, substitutions?: ReadonlyMap<number, Uint8Array>, connectors?: ReadonlyMap<string, Uint8Array>, computedResults?: ReadonlyArray<ComputedResult>,
148
163
  /** When given, receives the lightest derivation's rule applications — the
149
164
  * full adapted A*LD proof tree as classified {@link DerivationStep}s — for the TOP
150
165
  * cover only (a recursive recompletion solves its own sub-cover and is not
@@ -63,6 +63,7 @@ function readCover(derivation) {
63
63
  bytes: out.bytes,
64
64
  rec: out.rec,
65
65
  node: out.node,
66
+ computed: out.computed,
66
67
  });
67
68
  }
68
69
  node = node.premises[0];
@@ -205,7 +206,7 @@ export class GraphSearch {
205
206
  * Any learnt connector between two rewrites is spliced IN by the in-search
206
207
  * connector rule (see {@link outRules}), so the returned spans already carry
207
208
  * it — there is no post-pass. */
208
- cover(queryLen, sites, conceptTarget, leaves, splits, substitutions, connectors, computedResults,
209
+ cover(queryLen, sites, conceptTarget, leaves, splits, starts, substitutions, connectors, computedResults,
209
210
  /** When given, receives the lightest derivation's rule applications — the
210
211
  * full adapted A*LD proof tree as classified {@link DerivationStep}s — for the TOP
211
212
  * cover only (a recursive recompletion solves its own sub-cover and is not
@@ -221,6 +222,7 @@ export class GraphSearch {
221
222
  sites,
222
223
  leaves,
223
224
  splits,
225
+ starts,
224
226
  }, conceptTarget, substitutions, connectors, computedResults, onDerivation);
225
227
  }
226
228
  /** Build the deduction system for one span and return its lightest cover's
@@ -237,7 +239,7 @@ export class GraphSearch {
237
239
  * decomposes, two recomposes, any mix — and stops only when it reaches a node
238
240
  * that leads nowhere new, never at an arbitrary count. */
239
241
  solve(spanLen, recognition, conceptTarget, substitutions, connectors, computedResults, onDerivation) {
240
- const system = this.buildSearch(spanLen, recognition.sites, conceptTarget, recognition.leaves, recognition.splits, substitutions, connectors, computedResults);
242
+ const system = this.buildSearch(spanLen, recognition.sites, conceptTarget, recognition.leaves, recognition.splits, recognition.starts, substitutions, connectors, computedResults);
241
243
  const derivation = lightestDerivation(system);
242
244
  // When covering under a substitution map (articulation), a form→out rule is
243
245
  // the form EMITTING the asker's voice, not grounding to its own answer — so
@@ -257,8 +259,15 @@ export class GraphSearch {
257
259
  * adjacent fragments toward a known leaf (findLeaf) or branch (findBranch);
258
260
  * a completion fused with its neighbour may spell a deeper learned form the
259
261
  * flat probes can't name, recovered canonically by {@link resolve}. */
260
- buildSearch(queryLen, sites, conceptTarget, leaves, splits, substitutions, connectors, computedResults) {
262
+ buildSearch(queryLen, sites, conceptTarget, leaves, splits, starts, substitutions, connectors, computedResults) {
261
263
  const W = this.maxGroup; // fusible span ceiling (shortest composite bound)
264
+ // Same corpus-scale hub floor {@link atomIsHub}/{@link atomReach} (traverse.ts)
265
+ // derive for byte atoms — duplicated here rather than imported because this
266
+ // module is deliberately host-based (no MindContext), and both inputs
267
+ // (maxGroup, edgeSourceCount) are already in scope with no ctx needed. If
268
+ // the formula ever changes, it must change in BOTH places (see canonical.ts's
269
+ // header for the same write/read-side duplication convention).
270
+ const atomsAreHubs = Math.max(1, Math.ceil((this.store.edgeSourceCount() * W) / 256)) > Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
262
271
  const nodeBytes = (n) => this.store.bytesPrefix(n, ALL);
263
272
  // Content-addressed probes over the store's hash-cons maps — the same keys
264
273
  // training filled. No byte-by-byte trie walk.
@@ -350,6 +359,7 @@ export class GraphSearch {
350
359
  cover: true,
351
360
  rec: true,
352
361
  node: u.node,
362
+ computed: true,
353
363
  },
354
364
  cost: STEP,
355
365
  };
@@ -388,6 +398,8 @@ export class GraphSearch {
388
398
  return this.outRules(it, {
389
399
  W,
390
400
  splits,
401
+ starts,
402
+ atomsAreHubs,
391
403
  coversDone,
392
404
  outsByStart,
393
405
  outsByEnd,
@@ -845,7 +857,23 @@ export class GraphSearch {
845
857
  * enters the graph as a form the moment it names a node. */
846
858
  *fuse(l, r, ctx) {
847
859
  const bytes = concat2(l.bytes, r.bytes);
848
- let node = bytes.length <= ctx.W ? ctx.findLeafU(bytes) : undefined;
860
+ // A PURE leaf-leaf fuse (neither side already a recognised completion)
861
+ // is opportunistic cross-leaf recovery exactly like recognition.ts's own
862
+ // canonical chain — findLeaf/findBranch here have no idea WHY these two
863
+ // leaves are adjacent, only that their concatenation happens to spell a
864
+ // trained form ("hi" recovered from "W[hi]ch"). The same corpus-scale
865
+ // caution recognition.ts's `boundary` gate applies: trust it fully when
866
+ // `l.i` is a position the query's OWN fold chose as a boundary (real
867
+ // structural evidence); past the scale where atoms themselves stop
868
+ // discriminating, an interior offset's opportunistic match is noise, not
869
+ // a genuine cross-leaf recovery. A completion-involved fuse (l.rec ||
870
+ // r.rec) is a different, legitimate case — a rewrite growing into its
871
+ // neighbour — and is exempt, same as recognition.ts's rec-derived sites.
872
+ const trusted = l.rec || r.rec || ctx.starts.has(l.i) ||
873
+ !ctx.atomsAreHubs;
874
+ let node = (trusted && bytes.length <= ctx.W)
875
+ ? ctx.findLeafU(bytes)
876
+ : undefined;
849
877
  // Whether this pair ACTUALLY forms a 2-child branch — the hard evidence
850
878
  // that the fused bytes are a learned form worth keeping alive. Derived
851
879
  // from the same findBranchU probe that sets `node`; when false, the pair
@@ -853,7 +881,8 @@ export class GraphSearch {
853
881
  // node cannot contribute to any further fusion (findBranch needs two
854
882
  // nodes, resolve needs a completion, and findLeaf already had its chance).
855
883
  let pairFormsBranch = false;
856
- if (node === undefined && l.node !== undefined && r.node !== undefined) {
884
+ if (trusted && node === undefined && l.node !== undefined &&
885
+ r.node !== undefined) {
857
886
  node = ctx.findBranchU([l.node, r.node]);
858
887
  pairFormsBranch = node !== undefined;
859
888
  }
@@ -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
@@ -12,7 +12,14 @@ import { unexplainedLabel } from "../rationale.js";
12
12
  export function aluToMechanism(alu) {
13
13
  return {
14
14
  name: "alu",
15
- provenance: "cover",
15
+ // Not a cover derivation: cover.ts composes an answer by walking
16
+ // recognised query STRUCTURE; the ALU evaluates a recognised expression
17
+ // to its authoritative result and hands the bytes back untouched. It
18
+ // shares cover's near-zero floor (computation always wins, masked into
19
+ // cover's own search — see mechanisms/cover.ts), but the candidate this
20
+ // produces is not one of cover's derivations, so it carries its own
21
+ // honest label, the same way extract/cast/recall each carry theirs.
22
+ provenance: "alu",
16
23
  parse: (query) => alu.parse(query),
17
24
  async floor(_ctx, _query, pre, _worthRunning) {
18
25
  return pre.computed.length > 0 ? 0 : null;
@@ -70,19 +70,37 @@ const MIN_WEAVE = 2;
70
70
  *
71
71
  * Returns the array of {@link CastResult}s that fired (possibly empty). */
72
72
  export async function counterfactualTransfer(ctx, query, pre) {
73
+ // Opened unconditionally, at entry — the same convention recall.ts's
74
+ // recallByResonance and extraction.ts's extractBySkill use, so every exit
75
+ // path (five gates below, then the schemas themselves) closes through
76
+ // ONE scope and inspectRationale never hits a silent dead end. Only the
77
+ // first two gates duplicate floor()'s own admissible bound (query length,
78
+ // ranked anchor count) — required to stay in sync per this function's own
79
+ // doc comment above, and effectively dead through the ordinary pipeline
80
+ // (floor() returning null already stops run() from being called at all),
81
+ // but this function is also exported and callable directly, so they stay
82
+ // and get the same honest trace as everything past them.
83
+ const t = ctx.trace?.enter("counterfactual", [rItem(query, "query")]);
84
+ const fail = (note) => {
85
+ t?.done([], note);
86
+ return [];
87
+ };
73
88
  const quantum = ctx.space.maxGroup;
74
89
  if (query.length < 2 * quantum || ctx.store.edgeSourceCount() === 0) {
75
- return [];
90
+ return fail("query below the two-quantum floor, or no edges learnt yet");
76
91
  }
77
92
  const { roots, ranked } = await pre.attention();
78
- if (ranked.length < 2)
79
- return [];
93
+ if (ranked.length < 2) {
94
+ return fail(`only ${ranked.length} ranked anchor(s) — CAST needs at least two`);
95
+ }
80
96
  const weave = await pre.weave();
81
97
  const points = weave.points;
82
98
  const depth = weave.depth;
83
99
  const aligned = points.length;
84
- if (aligned < 2)
85
- return [];
100
+ if (aligned < 2) {
101
+ return fail(`only ${aligned} structure(s) aligned across the query — CAST needs ` +
102
+ `at least two to transfer between`);
103
+ }
86
104
  // ── Frame gate (half-dominance, weave-local) ─────────────────────────
87
105
  // A byte is FRAME when more than MIN_WEAVE aligned structures cover it
88
106
  // AND those structures are a majority of all aligned structures.
@@ -120,14 +138,20 @@ export async function counterfactualTransfer(ctx, query, pre) {
120
138
  const isRoot = (id) => roots.some((r) => r.anchor === id);
121
139
  // The weave must touch a COMMITTED point of attention: the dominant
122
140
  // structure itself, or another aligned point the climb committed to.
123
- if (!points.some((p) => isRoot(p.anchor)))
141
+ if (!points.some((p) => isRoot(p.anchor))) {
142
+ t?.done([
143
+ ...points.map((p) => rNode(ctx, p.anchor, "aligned")),
144
+ ...roots.map((r) => rNode(ctx, r.anchor, "committed-root")),
145
+ ], `${points.length} aligned structure(s), but none is one of the climb's ` +
146
+ `${roots.length} committed root(s) — CAST refuses to transfer through ` +
147
+ `content the climb itself never settled on`);
124
148
  return [];
149
+ }
125
150
  const woven = points.some((p) => p.runs.some((r) => !pre.rec.sites.some((s) => r.qs >= s.start && r.qe <= s.end)));
126
- if (!woven)
127
- return [];
128
- const t = ctx.trace?.enter("counterfactual", [
129
- rItem(query, "query"),
130
- ]);
151
+ if (!woven) {
152
+ return fail(`every aligned run restates a recognised query site — nothing was ` +
153
+ `actually WOVEN across structures, so there is nothing to transfer`);
154
+ }
131
155
  // Each schema tried below RECORDS its candidate (when it fires) rather than
132
156
  // returning immediately — every schema that succeeds contributes its own
133
157
  // candidate, and the grounding decider's own weight comparison (not CAST's
@@ -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";
@@ -121,11 +122,18 @@ export const coverMechanism = {
121
122
  return [];
122
123
  const connectors = await resolveConnectors(ctx, sites);
123
124
  let splits = rec.splits;
125
+ let starts = rec.starts;
124
126
  if (computed.length > 0) {
125
127
  splits = new Set(rec.splits);
128
+ starts = new Set(rec.starts);
126
129
  for (const u of computed) {
127
130
  splits.add(u.i);
128
131
  splits.add(u.j);
132
+ // A computation's own boundaries carry the same fold-level evidence
133
+ // a chunk boundary does — "computation always wins" (see the header
134
+ // comment) extends to being trusted ground for cross-leaf recovery.
135
+ starts.add(u.i);
136
+ starts.add(u.j);
129
137
  }
130
138
  }
131
139
  const concepts = await resolveConcepts(ctx, sites);
@@ -149,7 +157,7 @@ export const coverMechanism = {
149
157
  ])),
150
158
  ...computedResults.map((u) => rItem(u.bytes, "computed")),
151
159
  ], coverDeps.length ? coverDeps : undefined);
152
- const solved = ctx.search.cover(query.length, sites, concepts, rec.leaves, splits, undefined, connectors, computedResults, ctx.trace ? (steps) => traceDerivation(ctx, steps) : undefined);
160
+ const solved = ctx.search.cover(query.length, sites, concepts, rec.leaves, splits, starts, undefined, connectors, computedResults, ctx.trace ? (steps) => traceDerivation(ctx, steps) : undefined);
153
161
  const segs = solved && solved.segs;
154
162
  tCover?.done(segs === null
155
163
  ? []
@@ -158,6 +166,37 @@ export const coverMechanism = {
158
166
  : "lightest derivation: the chosen spans, left to right");
159
167
  if (segs === null)
160
168
  return [];
169
+ // A chosen span's SUBSTITUTED bytes (an edge followed from a recognised
170
+ // site, not the site's own literal text read back) that equal a byte
171
+ // span the query ALREADY CONTAINS elsewhere restates part of the
172
+ // question — never an answer (the same principle recall.ts's tiers
173
+ // apply to a whole-query projection: "a projection that is a proper
174
+ // byte-subspan of the query restates part of the question"). A
175
+ // recognised site that is itself an entire PRIOR TURN of a multi-turn
176
+ // query is exactly this shape: it carries a genuine learnt
177
+ // continuation, but that continuation is something the asker already
178
+ // said moments later in the SAME query, not a new answer — the cover
179
+ // search has no notion of "turn" to gate this itself, so the check
180
+ // belongs here, over the derivation it already chose.
181
+ const W = ctx.space.maxGroup;
182
+ for (const s of segs) {
183
+ if (!s.rec)
184
+ continue;
185
+ const literal = s.j - s.i === s.bytes.length &&
186
+ bytesEqual(s.bytes, query.subarray(s.i, s.j));
187
+ if (literal)
188
+ continue;
189
+ // A span shorter than one river window is exactly the "ice"/"c"
190
+ // case: a chain hop's short terminal coincidentally recurring as a
191
+ // substring of an unrelated longer word. Below the quantum, byte
192
+ // overlap is chance, not evidence — the same floor identityBar and
193
+ // reachThreshold hold every other structural-overlap claim to.
194
+ if (s.bytes.length >= W && s.bytes.length < query.length &&
195
+ indexOf(query, s.bytes, 0) >= 0) {
196
+ 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");
197
+ return [];
198
+ }
199
+ }
161
200
  const composed = liftAnswer(segs, query.length);
162
201
  if (composed === null)
163
202
  return [];