@hviana/sema 0.2.2 → 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.
@@ -111,7 +111,15 @@ export async function reason(ctx, query, answer, preConsumed, pre) {
111
111
  * When the consensus climb finds more than one dominant point, each
112
112
  * independent point grounds its own answer; they are bridged together
113
113
  * by any learnt connector the graph holds between them. */
114
- export async function fuseAttention(ctx, query, primary, pre) {
114
+ export async function fuseAttention(ctx, query, primary, pre,
115
+ /** True when `primary` never touched the consensus climb at all — e.g. a
116
+ * pure ALU computation, which has no anchor of its own. commitVotes
117
+ * ALWAYS admits the dominant root regardless of its vote (attention.ts:
118
+ * "roots.length === 0 || …") on the assumption a lone root already IS
119
+ * primary's own source; that assumption is exactly backwards when
120
+ * primary is unclimbed. Absent or false preserves the original
121
+ * behaviour exactly. */
122
+ unclimbed = false) {
115
123
  // When the answer is structurally drawn from the query itself
116
124
  // (extraction), it already spans all the query's pieces — fusion
117
125
  // would only add noise from unrelated stored contexts. The gate is
@@ -125,17 +133,31 @@ export async function fuseAttention(ctx, query, primary, pre) {
125
133
  // query, same k, same DF mode) — read them from Precomputed instead of
126
134
  // re-climbing, so even a traced response pays for the climb once.
127
135
  const forest = (await pre.attention()).roots;
128
- if (forest.length <= 1)
136
+ // A LONE root is ordinarily primary's own source — nothing to fuse. But
137
+ // when primary is unclimbed, the lone root was never checked against
138
+ // anything: it is admitted by commitVotes unconditionally, so it may be
139
+ // genuine consensus (Attention.breadth dominates — most of the query's
140
+ // OWN regions corroborate it) or a coincidental echo (breadth does not
141
+ // dominate — see test/35-attention-confidence). breadth is the SCALE-
142
+ // INVARIANT read of exactly this question: the raw IDF vote cannot serve
143
+ // here, since it is an absolute ln(N)-scaled quantity (a genuine root on
144
+ // a large store can score BELOW its own floor while a coincidental echo
145
+ // on a small one scores comfortably above its own, smaller, floor).
146
+ const lonePromotes = unclimbed && forest.length === 1 &&
147
+ forest[0].breadth > 0.5;
148
+ if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) {
129
149
  return primary;
150
+ }
130
151
  const pieces = [
131
152
  { start: forest[0].start, bytes: primary },
132
153
  ];
133
154
  const qv = pre.guide; // once, not per root
155
+ const rest = lonePromotes ? forest : forest.slice(1);
134
156
  const t = ctx.trace?.enter("fuseAttention", [
135
157
  rItem(primary, "primary"),
136
- ...forest.slice(1).map((r) => rNode(ctx, r.anchor, "point", r.vote)),
158
+ ...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
137
159
  ]);
138
- for (const root of forest.slice(1)) {
160
+ for (const root of rest) {
139
161
  const g = await project(ctx, root.anchor, qv);
140
162
  if (g === null || g.length === 0)
141
163
  continue;
@@ -41,8 +41,9 @@ function recogniseImpl(ctx, bytes) {
41
41
  const sites = [];
42
42
  const leaves = [];
43
43
  const splits = new Set();
44
+ const starts = new Set();
44
45
  if (bytes.length === 0)
45
- return { sites, leaves, splits };
46
+ return { sites, leaves, splits, starts };
46
47
  // Span-resolve memo for THIS call: the structural pass (sub-runs inside
47
48
  // leaf-parents) and the canonical pass (leaf-id chains) probe overlapping
48
49
  // spans, and each resolve() is a full fold of the sub-span (fresh subarray
@@ -78,7 +79,6 @@ function recogniseImpl(ctx, bytes) {
78
79
  }
79
80
  };
80
81
  // ── structural: the query's own perceived tree ──────────────────────
81
- const starts = new Set();
82
82
  starts.add(0);
83
83
  foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
84
84
  if (n.kids === null) {
@@ -105,7 +105,16 @@ function recogniseImpl(ctx, bytes) {
105
105
  leafOffsets.push(off);
106
106
  off += k.leaf?.length ?? 0;
107
107
  }
108
+ // Sub-spans starting at i > 0 begin INSIDE the chunk, at an offset the
109
+ // query's own fold did not itself choose as a boundary — the same
110
+ // opportunistic byte-atom-chain risk `tryChain`'s `boundary` gate
111
+ // guards below (see its comment). Only the chunk's own left edge
112
+ // (i === 0, already registered in `starts` above) carries the fold's
113
+ // evidence; interior sub-starts are exempt from the guard only while
114
+ // atoms themselves still discriminate at this corpus scale.
108
115
  for (let i = 0; i < n.kids.length; i++) {
116
+ if (i > 0 && atomsAreHubs)
117
+ break;
109
118
  const subIds = [];
110
119
  for (let j = i; j < n.kids.length; j++) {
111
120
  const kj = n.kids[j];
@@ -148,7 +157,20 @@ function recogniseImpl(ctx, bytes) {
148
157
  chunkEnd[p] = chunkLimit;
149
158
  }
150
159
  }
151
- const tryChain = (p, maxIds) => {
160
+ // A chain rebuilt from a NON-boundary offset (the query's own perceived
161
+ // cut, `starts`, never chose to segment here) is opportunistic: the same
162
+ // byte-atom coincidence the hub guard above already exists for, just
163
+ // spelled over 2+ leaves instead of 1. At small corpus scale that's fine
164
+ // — coincidence is rare and every chain is real evidence (see `atomIsHub`).
165
+ // Past the scale where atoms themselves stop discriminating, the same
166
+ // uniform-expectation argument bounds a CHAIN'S commonality too: it is at
167
+ // least as rare as its rarest atom, so a store where atoms are hubs makes
168
+ // interior chain reconstructions no more trustworthy than the atoms they
169
+ // are built from ("hi" resolving out of "W[hi]ch" is exactly this: two
170
+ // hub-scale atoms, chained at an offset nothing in the query's own fold
171
+ // selected). Chains that start ON a boundary carry the fold's own
172
+ // evidence instead and are exempt.
173
+ const tryChain = (p, maxIds, boundary) => {
152
174
  const first = leafFrom(p);
153
175
  if (!first)
154
176
  return;
@@ -164,6 +186,8 @@ function recogniseImpl(ctx, bytes) {
164
186
  pos = nx.end;
165
187
  if (store.findBranch(ids) === null)
166
188
  continue;
189
+ if (!boundary && atomsAreHubs)
190
+ continue;
167
191
  const id = resolveSpan(p, pos);
168
192
  if (id === null || id === prevId)
169
193
  continue;
@@ -173,11 +197,11 @@ function recogniseImpl(ctx, bytes) {
173
197
  };
174
198
  for (let p = 0; p < bytes.length; p++) {
175
199
  if (starts.has(p)) {
176
- tryChain(p, chainReach(W)); // boundary start — full reach
200
+ tryChain(p, chainReach(W), true); // boundary start — full reach
177
201
  }
178
202
  else {
179
203
  const limit = chunkEnd[p] + W;
180
- tryChain(p, Math.min(limit - p, chainReach(W)));
204
+ tryChain(p, Math.min(limit - p, chainReach(W)), false);
181
205
  }
182
206
  }
183
207
  // ── splits: a form boundary that does not fall on a leaf edge ────────
@@ -195,7 +219,7 @@ function recogniseImpl(ctx, bytes) {
195
219
  s.end,
196
220
  ])), `decompose the query into ${sites.length} learnt form(s) that lead somewhere` +
197
221
  ` (over ${leaves.length} perceived leaves)`);
198
- return { sites, leaves, splits };
222
+ return { sites, leaves, splits, starts };
199
223
  }
200
224
  /** Segment bytes using the geometry's own groupings — leaf-parent
201
225
  * nodes from the perceived tree, with consecutive bare leaves merged
@@ -6,6 +6,7 @@
6
6
  // The PROJECTIONS built on these walks (follow, conceptHop, reverseContext,
7
7
  // project) live in match.ts — the elementary match-and-project operation.
8
8
  import { cosine } from "../vec.js";
9
+ import { consensusFloor } from "../geometry.js";
9
10
  import { gistOf, read } from "./primitives.js";
10
11
  const structCaches = new WeakMap();
11
12
  function getStructCache(ctx) {
@@ -439,6 +440,20 @@ export function chooseNext(ctx, id, guide) {
439
440
  bestMass = mass;
440
441
  }
441
442
  }
443
+ // A pick among GENUINELY competing continuations still needs to clear the
444
+ // same genuine-corroboration floor {@link consensusFloor} draws for every
445
+ // other consumer that turns distinct-context support into a vote (the
446
+ // climb's recallByResonance/commitVotes) — below it, "most corroborated"
447
+ // is only the least-thin echo among several, not real evidence. Gated to
448
+ // corpus scale large enough that this echo is even possible (the same
449
+ // scale {@link atomIsHub} already switches on for the identical reason:
450
+ // at small N every edge IS the evidence there is). A hub id has no
451
+ // meaningful "distinct context" reading at all (id < 0 atoms are excluded
452
+ // from this path already, since chooseNext only ever sees real edges).
453
+ const N = corpusN(ctx);
454
+ if (capped.length > 1 && atomIsHub(ctx, N) && bestSupport < consensusFloor(N)) {
455
+ return undefined;
456
+ }
442
457
  // Trace is built lazily — the filter + map below only execute when a
443
458
  // trace listener is attached, so the common (no-trace) path pays only
444
459
  // for the prevCount calls in the loop above, never for extra rItemShort
@@ -34,6 +34,7 @@ export interface GraphSearchHost {
34
34
  sites: ReadonlyArray<Site>;
35
35
  leaves: ReadonlyArray<Leaf>;
36
36
  splits: ReadonlySet<number>;
37
+ starts: ReadonlySet<number>;
37
38
  };
38
39
  chooseNext?(node: number): number | undefined;
39
40
  }
@@ -44,6 +45,13 @@ export interface Recognition {
44
45
  leaves: Leaf[];
45
46
  /** Sub-leaf positions where a form boundary falls between leaf edges. */
46
47
  splits: Set<number>;
48
+ /** Leaf-parent (chunk) start positions from the query's OWN perceived
49
+ * fold — the positions the fold itself chose as a grouping boundary, as
50
+ * opposed to an offset a byte-level scan merely happens to land on. The
51
+ * one boundary signal opportunistic cross-leaf recovery (recognition's
52
+ * own canonical chains, the search's `fuse`) can lean on instead of
53
+ * ASCII/word heuristics: see the `boundary` gate in recognition.ts. */
54
+ starts: Set<number>;
47
55
  }
48
56
  /** How the consensus climb weights a region's Document-Frequency reach. */
49
57
  export type DFMode = "inverse" | "direct" | "combined";
@@ -56,6 +64,16 @@ export interface Attention {
56
64
  /** The union of the query byte-spans whose evidence supports this point. */
57
65
  start: number;
58
66
  end: number;
67
+ /** SCALE-INVARIANT confidence: the fraction of the query's OWN regions
68
+ * whose evidence this point accounts for (Σ RegionVote.absorbed among
69
+ * its contributors, over the query's total region count) — read PER-
70
+ * ANCHOR, unlike the raw IDF vote (an absolute, ln(N)-scaled quantity
71
+ * that means "strong" on a small store and "weak" on a large one for
72
+ * the SAME degree of genuine consensus). A point whose breadth clears
73
+ * `dominates` (> half the query's regions corroborate it) is real
74
+ * consensus; one that does not is a coincidental single-region echo —
75
+ * see test/35-attention-confidence.test.mjs. */
76
+ breadth: number;
59
77
  }
60
78
  /** Both read-outs of one consensus climb. */
61
79
  export interface AttentionRead {
@@ -89,6 +107,14 @@ export interface RegionVote {
89
107
  roots: readonly number[];
90
108
  w: number;
91
109
  wFocus: number;
110
+ /** How many of the query's ORIGINAL regions this one vote's evidence
111
+ * accounts for. 1 for an ordinary per-region vote (itself); for a
112
+ * cross-region junction vote, 1 (itself) plus however many individual
113
+ * votes it explained away (see crossRegionVotes) — the junction speaks
114
+ * for all of them at once, and breadth accounting must not undercount it
115
+ * to "one region" just because it collapsed to one pooled axiom.
116
+ * Defaults to 1 when absent. */
117
+ absorbed?: number;
92
118
  }
93
119
  /** The edge-bearing contexts reached by climbing from a node, plus saturation info. */
94
120
  export interface AncestorReach {
@@ -26,6 +26,21 @@ export function liftAnswer(segs, queryLen) {
26
26
  return null;
27
27
  if (recognised.length === 1) {
28
28
  const s = segs[recognised[0]];
29
+ // A COMPUTED span's query-side width is operand digit-count, not
30
+ // evidence of how much of the query's meaning it accounts for — the
31
+ // half-dominance check below (built for a genuinely RECOGNISED learned
32
+ // form) is not a valid framing signal for it (see the `computed` field
33
+ // doc on Seg/GItem): "1000 - 421" outweighs "what is …?" by width only
34
+ // because the operands are big, not because the framing matters less.
35
+ // A LITERAL PREFIX before a computed span is unambiguous framing
36
+ // regardless of width — an arithmetic expression is never itself
37
+ // preceded by more literal computed content, so anything literal before
38
+ // it is question wording ("what is ", "compute ") to lift clear of.
39
+ // With no prefix (s.i === 0) the span is judged by the ordinary
40
+ // half-dominance rule below, which already correctly keeps a short
41
+ // trailing glue byte ("2+2." → "4.", the span dominates a 4-byte query).
42
+ if (s.computed && s.i > 0)
43
+ return s.bytes;
29
44
  if (dominates(s.j - s.i, queryLen)) {
30
45
  return concatBytes(segs.map((x) => x.bytes));
31
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -105,6 +105,7 @@ export async function articulate(
105
105
  new Map(),
106
106
  ans.leaves,
107
107
  ans.splits,
108
+ ans.starts,
108
109
  substitutions,
109
110
  undefined,
110
111
  undefined,
@@ -417,6 +417,9 @@ export function poolVotes(
417
417
  votes: Map<number, number>;
418
418
  votesIdf: Map<number, number>;
419
419
  support: Map<number, { start: number; end: number; w: number }>;
420
+ /** Per-anchor SCALE-INVARIANT support: Σ RegionVote.absorbed over the
421
+ * distinct contributing regions — see Attention.breadth. */
422
+ regionSupport: Map<number, number>;
420
423
  steps: DerivationStep[];
421
424
  } {
422
425
  const eligible: number[] = [];
@@ -496,6 +499,7 @@ export function poolVotes(
496
499
  number,
497
500
  { start: number; end: number; w: number }
498
501
  >();
502
+ const regionSupport = new Map<number, number>();
499
503
  const steps: DerivationStep[] = [];
500
504
  let order = 0;
501
505
  for (const pc of pool.values()) {
@@ -503,13 +507,16 @@ export function poolVotes(
503
507
  votes.set(pc.item.id, pc.cost);
504
508
  const premises: DerivationItem[] = [];
505
509
  const seenRi = new Set<number>();
510
+ let breadthSum = 0;
506
511
  for (const c of pc.contributions) {
507
512
  const p0 = c.premises[0].item;
508
513
  if (p0.kind !== "region" || seenRi.has(p0.ri)) continue;
509
514
  seenRi.add(p0.ri);
510
515
  const rv = regionVotes[p0.ri];
516
+ breadthSum += rv.absorbed ?? 1;
511
517
  premises.push({ kind: "form", span: [rv.start, rv.end] });
512
518
  }
519
+ regionSupport.set(pc.item.id, breadthSum);
513
520
  steps.push({
514
521
  order: order++,
515
522
  move: "pool-vote",
@@ -536,7 +543,7 @@ export function poolVotes(
536
543
  }
537
544
  }
538
545
  }
539
- return { votes, votesIdf, support, steps };
546
+ return { votes, votesIdf, support, regionSupport, steps };
540
547
  }
541
548
 
542
549
  export function commitVotes(
@@ -545,6 +552,7 @@ export function commitVotes(
545
552
  votes: Map<number, number>;
546
553
  votesIdf: Map<number, number>;
547
554
  support: Map<number, { start: number; end: number; w: number }>;
555
+ regionSupport: Map<number, number>;
548
556
  steps: DerivationStep[];
549
557
  },
550
558
  sat: SaturationInfo,
@@ -552,16 +560,27 @@ export function commitVotes(
552
560
  regionVoter: ReadonlyArray<{ id: number; score: number; w: number } | null>,
553
561
  N: number,
554
562
  ): AttentionRead {
555
- const { votes, votesIdf, support, steps } = pooled;
563
+ const { votes, votesIdf, support, regionSupport, steps } = pooled;
556
564
  if (votes.size === 0) {
557
565
  traceAttention(ctx, regions, regionVoter, [], steps);
558
566
  return { roots: [], ranked: [] };
559
567
  }
560
568
 
569
+ // SCALE-INVARIANT confidence — see Attention.breadth's doc. regions.length
570
+ // is the query's OWN full candidate count (most never vote at all), the
571
+ // same denominator the "N of M sub-regions voted" rationale text already
572
+ // reports; regionSupport is that same accounting read PER ANCHOR.
573
+ const totalRegions = Math.max(1, regions.length);
561
574
  const ranked = [...votes.entries()]
562
575
  .map(([anchor, vote]) => {
563
576
  const s = support.get(anchor)!;
564
- return { anchor, vote, start: s.start, end: s.end };
577
+ return {
578
+ anchor,
579
+ vote,
580
+ start: s.start,
581
+ end: s.end,
582
+ breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
583
+ };
565
584
  })
566
585
  .sort((a, b) => b.vote - a.vote);
567
586
 
@@ -1016,14 +1035,6 @@ async function crossRegionVotes(
1016
1035
  spanStart = Math.min(spanStart, regions[ei].start);
1017
1036
  spanEnd = Math.max(spanEnd, regions[ei].end);
1018
1037
  }
1019
- out.push({
1020
- start: spanStart,
1021
- end: spanEnd,
1022
- canonicalFailed: false, // content-addressed: never saturation-masked
1023
- roots: reach.roots,
1024
- w,
1025
- wFocus: w,
1026
- });
1027
1038
  consumed.add(cand[a]);
1028
1039
  consumed.add(cand[b]);
1029
1040
  for (const ei of bestExtras) consumed.add(ei);
@@ -1033,14 +1044,33 @@ async function crossRegionVotes(
1033
1044
  // vote's bytes are literally part of the learnt whole), and FULL root
1034
1045
  // disjointness is the disagreement test: a vote sharing even one root
1035
1046
  // with the junction corroborates it and keeps its say elsewhere.
1047
+ // Counted BEFORE pushing the junction's own vote below: each ORIGINAL
1048
+ // region this ascent explains away is evidence the junction speaks
1049
+ // for, not evidence lost — `absorbed` (RegionVote's breadth-accounting
1050
+ // field) must credit the junction with all of it, not just the ONE
1051
+ // pooled axiom it collapses to.
1036
1052
  const containerBytes = cachedRead(ctx, cache, best.id, cap);
1037
1053
  const jointRoots = new Set(reach.roots);
1054
+ let explainedAway = 0;
1038
1055
  for (const rv of rvs.votes) {
1039
1056
  if (rv.roots.some((r) => jointRoots.has(r))) continue;
1040
1057
  const bytes = query.subarray(rv.start, rv.end);
1041
- if (indexOf(containerBytes, bytes, 0) >= 0) superseded.add(rv);
1058
+ if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
1059
+ superseded.add(rv);
1060
+ explainedAway++;
1061
+ }
1042
1062
  }
1043
1063
 
1064
+ out.push({
1065
+ start: spanStart,
1066
+ end: spanEnd,
1067
+ canonicalFailed: false, // content-addressed: never saturation-masked
1068
+ roots: reach.roots,
1069
+ w,
1070
+ wFocus: w,
1071
+ absorbed: 1 + explainedAway,
1072
+ });
1073
+
1044
1074
  const label = [cand[a], cand[b], ...bestExtras]
1045
1075
  .sort((x, y) => regions[x].start - regions[y].start)
1046
1076
  .map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
@@ -96,6 +96,18 @@ export type GItem =
96
96
  cover: boolean;
97
97
  rec: boolean;
98
98
  node?: number;
99
+ /** Set only for a {@link ComputedResult} axiom (an extension's derived
100
+ * value, e.g. ALU arithmetic) — as opposed to a genuinely RECOGNISED
101
+ * learned form. Both set `rec: true` (a computation bridges the cover
102
+ * exactly like a learned terminal answer), but they mean different
103
+ * things for `i..j`'s WIDTH: a recognised form's query-span width is
104
+ * real evidence of how much of the query's meaning it accounts for
105
+ * (liftAnswer's half-dominance framing decision); a computed span's
106
+ * width is operand digit-count, uncorrelated with meaning ("1000 - 421"
107
+ * is wider than "15 * 7" only because the numbers are bigger, not
108
+ * because subtraction is more "the point" of its query). See
109
+ * {@link liftAnswer}. */
110
+ computed?: boolean;
99
111
  };
100
112
  type OutItem = Extract<GItem, { kind: "out" }>;
101
113
 
@@ -138,6 +150,9 @@ export interface Seg {
138
150
  bytes: Uint8Array;
139
151
  rec: boolean;
140
152
  node?: number;
153
+ /** See the `computed` field of the "out" {@link GItem} — set only for an
154
+ * extension's derived value, never a genuinely recognised learned form. */
155
+ computed?: boolean;
141
156
  }
142
157
 
143
158
  /** Read the chosen spans back off a derivation: the goal is a chain of bridge
@@ -155,6 +170,7 @@ function readCover(derivation: Derivation<GItem>): Seg[] {
155
170
  bytes: out.bytes,
156
171
  rec: out.rec,
157
172
  node: out.node,
173
+ computed: out.computed,
158
174
  });
159
175
  }
160
176
  node = node.premises[0];
@@ -356,6 +372,7 @@ export class GraphSearch {
356
372
  conceptTarget: ReadonlyMap<number, number>,
357
373
  leaves: ReadonlyArray<Leaf>,
358
374
  splits: ReadonlySet<number>,
375
+ starts: ReadonlySet<number>,
359
376
  substitutions?: ReadonlyMap<number, Uint8Array>,
360
377
  connectors?: ReadonlyMap<string, Uint8Array>,
361
378
  computedResults?: ReadonlyArray<ComputedResult>,
@@ -377,6 +394,7 @@ export class GraphSearch {
377
394
  sites,
378
395
  leaves,
379
396
  splits,
397
+ starts,
380
398
  },
381
399
  conceptTarget,
382
400
  substitutions,
@@ -405,6 +423,7 @@ export class GraphSearch {
405
423
  sites: ReadonlyArray<Site>;
406
424
  leaves: ReadonlyArray<Leaf>;
407
425
  splits: ReadonlySet<number>;
426
+ starts: ReadonlySet<number>;
408
427
  },
409
428
  conceptTarget: ReadonlyMap<number, number>,
410
429
  substitutions?: ReadonlyMap<number, Uint8Array>,
@@ -418,6 +437,7 @@ export class GraphSearch {
418
437
  conceptTarget,
419
438
  recognition.leaves,
420
439
  recognition.splits,
440
+ recognition.starts,
421
441
  substitutions,
422
442
  connectors,
423
443
  computedResults,
@@ -448,11 +468,22 @@ export class GraphSearch {
448
468
  conceptTarget: ReadonlyMap<number, number>,
449
469
  leaves: ReadonlyArray<Leaf>,
450
470
  splits: ReadonlySet<number>,
471
+ starts: ReadonlySet<number>,
451
472
  substitutions?: ReadonlyMap<number, Uint8Array>,
452
473
  connectors?: ReadonlyMap<string, Uint8Array>,
453
474
  computedResults?: ReadonlyArray<ComputedResult>,
454
475
  ): DeductionSystem<GItem> {
455
476
  const W = this.maxGroup; // fusible span ceiling (shortest composite bound)
477
+ // Same corpus-scale hub floor {@link atomIsHub}/{@link atomReach} (traverse.ts)
478
+ // derive for byte atoms — duplicated here rather than imported because this
479
+ // module is deliberately host-based (no MindContext), and both inputs
480
+ // (maxGroup, edgeSourceCount) are already in scope with no ctx needed. If
481
+ // the formula ever changes, it must change in BOTH places (see canonical.ts's
482
+ // header for the same write/read-side duplication convention).
483
+ const atomsAreHubs = Math.max(
484
+ 1,
485
+ Math.ceil((this.store.edgeSourceCount() * W) / 256),
486
+ ) > Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
456
487
  const nodeBytes = (n: number) => this.store.bytesPrefix(n, ALL);
457
488
  // Content-addressed probes over the store's hash-cons maps — the same keys
458
489
  // training filled. No byte-by-byte trie walk.
@@ -550,6 +581,7 @@ export class GraphSearch {
550
581
  cover: true,
551
582
  rec: true,
552
583
  node: u.node,
584
+ computed: true,
553
585
  },
554
586
  cost: STEP,
555
587
  };
@@ -588,6 +620,8 @@ export class GraphSearch {
588
620
  return this.outRules(it, {
589
621
  W,
590
622
  splits,
623
+ starts,
624
+ atomsAreHubs,
591
625
  coversDone,
592
626
  outsByStart,
593
627
  outsByEnd,
@@ -949,6 +983,8 @@ export class GraphSearch {
949
983
  ctx: {
950
984
  W: number;
951
985
  splits: ReadonlySet<number>;
986
+ starts: ReadonlySet<number>;
987
+ atomsAreHubs: boolean;
952
988
  coversDone: Set<number>;
953
989
  outsByStart: Map<number, OutItem[]>;
954
990
  outsByEnd: Map<number, OutItem[]>;
@@ -1095,12 +1131,30 @@ export class GraphSearch {
1095
1131
  r: OutItem,
1096
1132
  ctx: {
1097
1133
  W: number;
1134
+ starts: ReadonlySet<number>;
1135
+ atomsAreHubs: boolean;
1098
1136
  findLeafU: (b: Uint8Array) => number | undefined;
1099
1137
  findBranchU: (k: number[]) => number | undefined;
1100
1138
  },
1101
1139
  ): Iterable<Rule<GItem>> {
1102
1140
  const bytes = concat2(l.bytes, r.bytes);
1103
- let node = bytes.length <= ctx.W ? ctx.findLeafU(bytes) : undefined;
1141
+ // A PURE leaf-leaf fuse (neither side already a recognised completion)
1142
+ // is opportunistic cross-leaf recovery exactly like recognition.ts's own
1143
+ // canonical chain — findLeaf/findBranch here have no idea WHY these two
1144
+ // leaves are adjacent, only that their concatenation happens to spell a
1145
+ // trained form ("hi" recovered from "W[hi]ch"). The same corpus-scale
1146
+ // caution recognition.ts's `boundary` gate applies: trust it fully when
1147
+ // `l.i` is a position the query's OWN fold chose as a boundary (real
1148
+ // structural evidence); past the scale where atoms themselves stop
1149
+ // discriminating, an interior offset's opportunistic match is noise, not
1150
+ // a genuine cross-leaf recovery. A completion-involved fuse (l.rec ||
1151
+ // r.rec) is a different, legitimate case — a rewrite growing into its
1152
+ // neighbour — and is exempt, same as recognition.ts's rec-derived sites.
1153
+ const trusted = l.rec || r.rec || ctx.starts.has(l.i) ||
1154
+ !ctx.atomsAreHubs;
1155
+ let node = (trusted && bytes.length <= ctx.W)
1156
+ ? ctx.findLeafU(bytes)
1157
+ : undefined;
1104
1158
  // Whether this pair ACTUALLY forms a 2-child branch — the hard evidence
1105
1159
  // that the fused bytes are a learned form worth keeping alive. Derived
1106
1160
  // from the same findBranchU probe that sets `node`; when false, the pair
@@ -1108,7 +1162,10 @@ export class GraphSearch {
1108
1162
  // node cannot contribute to any further fusion (findBranch needs two
1109
1163
  // nodes, resolve needs a completion, and findLeaf already had its chance).
1110
1164
  let pairFormsBranch = false;
1111
- if (node === undefined && l.node !== undefined && r.node !== undefined) {
1165
+ if (
1166
+ trusted && node === undefined && l.node !== undefined &&
1167
+ r.node !== undefined
1168
+ ) {
1112
1169
  node = ctx.findBranchU([l.node, r.node]);
1113
1170
  pairFormsBranch = node !== undefined;
1114
1171
  }
@@ -16,7 +16,14 @@ import type { PipelineMechanism } from "../pipeline-mechanism.js";
16
16
  export function aluToMechanism(alu: Alu): PipelineMechanism {
17
17
  return {
18
18
  name: "alu",
19
- provenance: "cover",
19
+ // Not a cover derivation: cover.ts composes an answer by walking
20
+ // recognised query STRUCTURE; the ALU evaluates a recognised expression
21
+ // to its authoritative result and hands the bytes back untouched. It
22
+ // shares cover's near-zero floor (computation always wins, masked into
23
+ // cover's own search — see mechanisms/cover.ts), but the candidate this
24
+ // produces is not one of cover's derivations, so it carries its own
25
+ // honest label, the same way extract/cast/recall each carry theirs.
26
+ provenance: "alu",
20
27
  parse: (query) => alu.parse(query),
21
28
  async floor(_ctx, _query, pre, _worthRunning) {
22
29
  return pre.computed.length > 0 ? 0 : null;
@@ -102,18 +102,43 @@ export async function counterfactualTransfer(
102
102
  query: Uint8Array,
103
103
  pre: Precomputed,
104
104
  ): Promise<CastResult[]> {
105
+ // Opened unconditionally, at entry — the same convention recall.ts's
106
+ // recallByResonance and extraction.ts's extractBySkill use, so every exit
107
+ // path (five gates below, then the schemas themselves) closes through
108
+ // ONE scope and inspectRationale never hits a silent dead end. Only the
109
+ // first two gates duplicate floor()'s own admissible bound (query length,
110
+ // ranked anchor count) — required to stay in sync per this function's own
111
+ // doc comment above, and effectively dead through the ordinary pipeline
112
+ // (floor() returning null already stops run() from being called at all),
113
+ // but this function is also exported and callable directly, so they stay
114
+ // and get the same honest trace as everything past them.
115
+ const t = ctx.trace?.enter("counterfactual", [rItem(query, "query")]);
116
+ const fail = (note: string): CastResult[] => {
117
+ t?.done([], note);
118
+ return [];
119
+ };
120
+
105
121
  const quantum = ctx.space.maxGroup;
106
122
  if (query.length < 2 * quantum || ctx.store.edgeSourceCount() === 0) {
107
- return [];
123
+ return fail("query below the two-quantum floor, or no edges learnt yet");
108
124
  }
109
125
  const { roots, ranked } = await pre.attention();
110
- if (ranked.length < 2) return [];
126
+ if (ranked.length < 2) {
127
+ return fail(
128
+ `only ${ranked.length} ranked anchor(s) — CAST needs at least two`,
129
+ );
130
+ }
111
131
 
112
132
  const weave = await pre.weave();
113
133
  const points = weave.points;
114
134
  const depth = weave.depth;
115
135
  const aligned = points.length;
116
- if (aligned < 2) return [];
136
+ if (aligned < 2) {
137
+ return fail(
138
+ `only ${aligned} structure(s) aligned across the query — CAST needs ` +
139
+ `at least two to transfer between`,
140
+ );
141
+ }
117
142
 
118
143
  type Point = typeof points[0];
119
144
 
@@ -154,18 +179,31 @@ export async function counterfactualTransfer(
154
179
  const isRoot = (id: number) => roots.some((r) => r.anchor === id);
155
180
  // The weave must touch a COMMITTED point of attention: the dominant
156
181
  // structure itself, or another aligned point the climb committed to.
157
- if (!points.some((p) => isRoot(p.anchor))) return [];
182
+ if (!points.some((p) => isRoot(p.anchor))) {
183
+ t?.done(
184
+ [
185
+ ...points.map((p) => rNode(ctx, p.anchor, "aligned")),
186
+ ...roots.map((r) => rNode(ctx, r.anchor, "committed-root")),
187
+ ],
188
+ `${points.length} aligned structure(s), but none is one of the climb's ` +
189
+ `${roots.length} committed root(s) — CAST refuses to transfer through ` +
190
+ `content the climb itself never settled on`,
191
+ );
192
+ return [];
193
+ }
158
194
 
159
195
  const woven = points.some((p) =>
160
196
  p.runs.some((r) =>
161
197
  !pre.rec.sites.some((s) => r.qs >= s.start && r.qe <= s.end)
162
198
  )
163
199
  );
164
- if (!woven) return [];
200
+ if (!woven) {
201
+ return fail(
202
+ `every aligned run restates a recognised query site — nothing was ` +
203
+ `actually WOVEN across structures, so there is nothing to transfer`,
204
+ );
205
+ }
165
206
 
166
- const t = ctx.trace?.enter("counterfactual", [
167
- rItem(query, "query"),
168
- ]);
169
207
  // Each schema tried below RECORDS its candidate (when it fires) rather than
170
208
  // returning immediately — every schema that succeeds contributes its own
171
209
  // candidate, and the grounding decider's own weight comparison (not CAST's