@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.
@@ -159,11 +159,18 @@ export const coverMechanism: PipelineMechanism = {
159
159
 
160
160
  const connectors = await resolveConnectors(ctx, sites);
161
161
  let splits = rec.splits;
162
+ let starts = rec.starts;
162
163
  if (computed.length > 0) {
163
164
  splits = new Set(rec.splits);
165
+ starts = new Set(rec.starts);
164
166
  for (const u of computed) {
165
167
  splits.add(u.i);
166
168
  splits.add(u.j);
169
+ // A computation's own boundaries carry the same fold-level evidence
170
+ // a chunk boundary does — "computation always wins" (see the header
171
+ // comment) extends to being trusted ground for cross-leaf recovery.
172
+ starts.add(u.i);
173
+ starts.add(u.j);
167
174
  }
168
175
  }
169
176
  const concepts = await resolveConcepts(ctx, sites);
@@ -199,6 +206,7 @@ export const coverMechanism: PipelineMechanism = {
199
206
  concepts,
200
207
  rec.leaves,
201
208
  splits,
209
+ starts,
202
210
  undefined,
203
211
  connectors,
204
212
  computedResults,
@@ -70,43 +70,118 @@ export async function extractBySkill(
70
70
  if (ranked.length === 0) {
71
71
  return fail("no consensus anchor — no skill to apply");
72
72
  }
73
- let exemplar: SkillInfo | null = null;
74
- let chosenAnchor: number | null = null;
75
- let skipped = 0;
76
- for (const cand of ranked) {
77
- const ex = await pre.spanShapedOf(cand.anchor);
78
- if (ex) {
79
- exemplar = ex;
80
- chosenAnchor = cand.anchor;
81
- break;
73
+
74
+ // Try ranked anchors IN ORDER until one yields a USABLE extraction — not
75
+ // merely a span-shaped exemplar, but one whose extracted span clears the
76
+ // same one-river-fold quantum (W) cover.ts's restatedSpan gate already
77
+ // treats as the floor below which byte overlap is chance, not evidence.
78
+ // isSpanShaped (spanShapedOf) is a deliberately permissive sparse-
79
+ // subsequence check — see the section note below — so it accepts exemplars
80
+ // whose relation to the query is coincidental gap-matching, not genuine
81
+ // structure. Stopping at the FIRST such exemplar let a coincidental match
82
+ // early in the ranked list win outright and read out a sub-quantum
83
+ // fragment (observed: a 3-byte "Hel" pulled from an unrelated exemplar,
84
+ // while a later ranked anchor would have read the query's own "Hello…"
85
+ // correctly). Trying further anchors when one produces nothing usable is
86
+ // the same idiom this loop already uses for non-exemplars — extended to
87
+ // cover a bad extraction, not just a structural non-match.
88
+ //
89
+ // The retry is bounded at pre.k — the SAME evidence-breadth constant every
90
+ // other consumer of a ranked list already self-limits to (resonance, the
91
+ // weave, the climb itself; see Precomputed.k's own doc comment) — not the
92
+ // full ranked list. locate()'s frame match has an EXACT-byte tier with no
93
+ // significance correction of its own (short W-byte frames are cheap to
94
+ // match by pure chance), so trying every ranked anchor turns that per-
95
+ // anchor chance into a near-certainty over enough attempts: on a pure-
96
+ // gibberish query, 170 anchors deep found an unrelated Zulu exemplar whose
97
+ // short frame happened to byte-match, producing "xyzzy pl" — a coincidence
98
+ // no different in kind from the "RaBitQ estimate overshot the reach bar
99
+ // and grounded pure gibberish" failure recall.ts's own significance
100
+ // correction exists to prevent (see recallByResonance's reach-threshold
101
+ // comment). Bounding the search to the ranked list's own top-k restores
102
+ // the "genuinely relevant but not root-significant" exemplars this loop
103
+ // was built for, without the unbounded tail's chance collisions.
104
+ const W = ctx.space.maxGroup;
105
+ const searched = ranked.slice(0, pre.k);
106
+ let shapeMisses = 0;
107
+ let subQuantum = 0;
108
+ for (const cand of searched) {
109
+ const exemplar = await pre.spanShapedOf(cand.anchor);
110
+ if (!exemplar) {
111
+ shapeMisses++;
112
+ continue;
113
+ }
114
+ const built = buildFromExemplar(ctx, query, pre, exemplar);
115
+ if (built === null || built.bytes.length < W) {
116
+ subQuantum++;
117
+ continue;
118
+ }
119
+ if (shapeMisses > 0 || subQuantum > 0) {
120
+ ctx.trace?.step(
121
+ "trySkillAnchors",
122
+ [
123
+ rItem(
124
+ query.subarray(0, 0),
125
+ `skipped ${shapeMisses + subQuantum}`,
126
+ ),
127
+ rNode(ctx, cand.anchor, "chosen"),
128
+ ],
129
+ [],
130
+ `skipped ${shapeMisses} non-exemplar and ${subQuantum} sub-quantum ` +
131
+ `anchor(s) before one yielded a usable extraction`,
132
+ );
82
133
  }
83
- skipped++;
134
+ t?.done(
135
+ [rItem(built.bytes, "extracted")],
136
+ built.pieces === 1
137
+ ? `apply a learnt extraction skill — read the analogous span of the query` +
138
+ ` framed like "${
139
+ decodeText(exemplar.answerBytes)
140
+ }" sits in its exemplar`
141
+ : `apply a learnt MULTI-PIECE skill — read ${built.pieces} analogous` +
142
+ ` pieces of the query and synthesize them like "${
143
+ decodeText(exemplar.answerBytes)
144
+ }"`,
145
+ );
146
+ return {
147
+ bytes: built.bytes,
148
+ accounted: built.accounted,
149
+ unexplained: unexplainedLabel(query, built.accounted),
150
+ };
84
151
  }
85
- if (exemplar === null) {
152
+ if (shapeMisses === searched.length) {
86
153
  ctx.trace?.step(
87
154
  "trySkillAnchors",
88
155
  [],
89
156
  [],
90
- `none of ${ranked.length} ranked anchor(s) is a span-shaped skill exemplar`,
157
+ `none of the top ${searched.length} ranked anchor(s) (of ${ranked.length} total) ` +
158
+ `is a span-shaped skill exemplar`,
91
159
  );
92
160
  return fail("no consensus root is a span-shaped skill exemplar");
93
161
  }
94
- if (skipped > 0) {
95
- ctx.trace?.step(
96
- "trySkillAnchors",
97
- [
98
- rItem(query.subarray(0, 0), `skipped ${skipped}`),
99
- rNode(ctx, chosenAnchor!, "chosen"),
100
- ],
101
- [],
102
- `skipped ${skipped} anchor(s) that are not skill exemplars`,
103
- );
104
- }
162
+ return fail(
163
+ "no ranked anchor yielded an extraction at or above the quantum floor",
164
+ );
165
+ }
166
+
167
+ /** Build the extracted bytes for ONE already-accepted span-shaped exemplar —
168
+ * factored out of {@link extractBySkill} so its anchor loop can try
169
+ * successive ranked candidates instead of committing to the first
170
+ * structural match. Null when the exemplar's answer does not decompose
171
+ * against its context, or no piece's frame locates in the query. */
172
+ function buildFromExemplar(
173
+ ctx: MindContext,
174
+ query: Uint8Array,
175
+ pre: Precomputed,
176
+ exemplar: SkillInfo,
177
+ ):
178
+ | { bytes: Uint8Array; accounted: Array<[number, number]>; pieces: number }
179
+ | null {
105
180
  const { contextBytes, answerBytes } = exemplar;
106
181
 
107
182
  const ansCtxRuns = answerRunsInContext(ctx, contextBytes, answerBytes);
108
183
  if (ansCtxRuns === null || ansCtxRuns.length === 0) {
109
- return fail("answer is not a subsequence of the context");
184
+ return null;
110
185
  }
111
186
 
112
187
  if (ansCtxRuns.length > 1) {
@@ -200,25 +275,11 @@ export async function extractBySkill(
200
275
  if (preBounded && postBounded) accounted.push([start, end]);
201
276
  }
202
277
  if (pieces.length === 0) {
203
- return fail("no answer piece's frame located in the query");
278
+ return null;
204
279
  }
205
280
 
206
281
  const out = pieces.length === 1 ? pieces[0] : concatBytes(pieces);
207
- t?.done(
208
- [rItem(out, "extracted")],
209
- pieces.length === 1
210
- ? `apply a learnt extraction skill — read the analogous span of the query` +
211
- ` framed like "${decodeText(answerBytes)}" sits in its exemplar`
212
- : `apply a learnt MULTI-PIECE skill — read ${pieces.length} analogous` +
213
- ` pieces of the query and synthesize them like "${
214
- decodeText(answerBytes)
215
- }"`,
216
- );
217
- return {
218
- bytes: out,
219
- accounted,
220
- unexplained: unexplainedLabel(query, accounted),
221
- };
282
+ return { bytes: out, accounted, pieces: pieces.length };
222
283
  }
223
284
 
224
285
  // ── The two span-shape readings: OPEN acceptance vs. STRONG decomposition ──
package/src/mind/mind.ts CHANGED
@@ -266,9 +266,15 @@ export class Mind implements MindContext {
266
266
  sites: ReadonlyArray<Site>;
267
267
  leaves: ReadonlyArray<Leaf>;
268
268
  splits: ReadonlySet<number>;
269
+ starts: ReadonlySet<number>;
269
270
  } {
270
271
  const r = recognise(this, bytes);
271
- return { sites: r.sites, leaves: r.leaves, splits: r.splits };
272
+ return {
273
+ sites: r.sites,
274
+ leaves: r.leaves,
275
+ splits: r.splits,
276
+ starts: r.starts,
277
+ };
272
278
  }
273
279
 
274
280
  /** Disambiguate among multiple learnt continuations of the same context node.
@@ -288,8 +288,43 @@ export async function think(
288
288
  ? new Set<number>()
289
289
  : new Set(recognise(ctx, answer).sites.map((s) => s.payload));
290
290
  const reasoned = await reason(ctx, query, answer, preConsumed, pre);
291
- const fused = provenance === "recall" || provenance === "recall-echo"
292
- ? await fuseAttention(ctx, query, reasoned, pre)
291
+
292
+ // Fuse only when the query has a genuine REMAINDER no mechanism's
293
+ // structural evidence touched at all. `decided.accounted` alone
294
+ // undercounts this: it is a COST-LADDER quantity (cover.ts prices its
295
+ // masked/computed spans at near-zero and deliberately leaves them out of
296
+ // `accounted` so PASS-bridged bytes are still charged), not a coverage
297
+ // one — a query fully explained by one computed span plus bridged
298
+ // connectors can report `accounted: []` while nothing is actually left
299
+ // unexplained. The genuine remainder is what NEITHER the winning
300
+ // candidate's accounted spans NOR any recognised extension's computed
301
+ // span (`pre.computed` — every mechanism's parse() output, ALU included)
302
+ // ever touched. A remainder under one river-fold quantum (W, the same
303
+ // floor cover.ts's restatedSpan and the honesty-density bar above both
304
+ // use) is bridging punctuation/whitespace, never a second topic —
305
+ // observed: a single space between two fully-computed arithmetic spans
306
+ // ("2+2 3+3") registered as "unaccounted" and pulled in an unrelated
307
+ // corpus fact, corrupting "4 6" into "4 63".
308
+ const explained: Array<[number, number]> = [
309
+ ...decided.accounted,
310
+ ...pre.computed.map((u): [number, number] => [u.i, u.j]),
311
+ ];
312
+ const remainder = unaccounted(explained);
313
+ // Whether the winning candidate's entire recognised substance is
314
+ // COMPUTED — every accounted span exactly a pre.computed span, nothing
315
+ // from a genuinely recognised/climbed site. fuseAttention's lone-root
316
+ // shortcut assumes a single point of attention already IS primary's own
317
+ // source; that assumption is exactly backwards for a pure computation
318
+ // (an ALU result has no anchor of its own) — see fuseAttention's
319
+ // `unclimbed` parameter, gated there by Attention.breadth so a
320
+ // coincidental echo (which this flag alone cannot distinguish) is still
321
+ // rejected.
322
+ const unclimbed = decided.accounted.length > 0 &&
323
+ decided.accounted.every(([i, j]) =>
324
+ pre.computed.some((u) => u.i === i && u.j === j)
325
+ );
326
+ const fused = remainder >= ctx.space.maxGroup
327
+ ? await fuseAttention(ctx, query, reasoned, pre, unclimbed)
293
328
  : reasoned;
294
329
 
295
330
  done(
@@ -141,6 +141,14 @@ export async function fuseAttention(
141
141
  query: Uint8Array,
142
142
  primary: Uint8Array,
143
143
  pre: Precomputed,
144
+ /** True when `primary` never touched the consensus climb at all — e.g. a
145
+ * pure ALU computation, which has no anchor of its own. commitVotes
146
+ * ALWAYS admits the dominant root regardless of its vote (attention.ts:
147
+ * "roots.length === 0 || …") on the assumption a lone root already IS
148
+ * primary's own source; that assumption is exactly backwards when
149
+ * primary is unclimbed. Absent or false preserves the original
150
+ * behaviour exactly. */
151
+ unclimbed = false,
144
152
  ): Promise<Uint8Array> {
145
153
  // When the answer is structurally drawn from the query itself
146
154
  // (extraction), it already spans all the query's pieces — fusion
@@ -155,17 +163,32 @@ export async function fuseAttention(
155
163
  // query, same k, same DF mode) — read them from Precomputed instead of
156
164
  // re-climbing, so even a traced response pays for the climb once.
157
165
  const forest = (await pre.attention()).roots;
158
- if (forest.length <= 1) return primary;
166
+ // A LONE root is ordinarily primary's own source — nothing to fuse. But
167
+ // when primary is unclimbed, the lone root was never checked against
168
+ // anything: it is admitted by commitVotes unconditionally, so it may be
169
+ // genuine consensus (Attention.breadth dominates — most of the query's
170
+ // OWN regions corroborate it) or a coincidental echo (breadth does not
171
+ // dominate — see test/35-attention-confidence). breadth is the SCALE-
172
+ // INVARIANT read of exactly this question: the raw IDF vote cannot serve
173
+ // here, since it is an absolute ln(N)-scaled quantity (a genuine root on
174
+ // a large store can score BELOW its own floor while a coincidental echo
175
+ // on a small one scores comfortably above its own, smaller, floor).
176
+ const lonePromotes = unclimbed && forest.length === 1 &&
177
+ forest[0].breadth > 0.5;
178
+ if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) {
179
+ return primary;
180
+ }
159
181
 
160
182
  const pieces: Array<{ start: number; bytes: Uint8Array }> = [
161
183
  { start: forest[0].start, bytes: primary },
162
184
  ];
163
185
  const qv = pre.guide; // once, not per root
186
+ const rest = lonePromotes ? forest : forest.slice(1);
164
187
  const t = ctx.trace?.enter("fuseAttention", [
165
188
  rItem(primary, "primary"),
166
- ...forest.slice(1).map((r) => rNode(ctx, r.anchor, "point", r.vote)),
189
+ ...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
167
190
  ]);
168
- for (const root of forest.slice(1)) {
191
+ for (const root of rest) {
169
192
  const g = await project(ctx, root.anchor, qv);
170
193
  if (g === null || g.length === 0) continue;
171
194
  if (pieces.some((p) => indexOf(p.bytes, g, 0) >= 0)) continue;
@@ -52,7 +52,8 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
52
52
  const sites: Site[] = [];
53
53
  const leaves: Leaf[] = [];
54
54
  const splits = new Set<number>();
55
- if (bytes.length === 0) return { sites, leaves, splits };
55
+ const starts = new Set<number>();
56
+ if (bytes.length === 0) return { sites, leaves, splits, starts };
56
57
 
57
58
  // Span-resolve memo for THIS call: the structural pass (sub-runs inside
58
59
  // leaf-parents) and the canonical pass (leaf-id chains) probe overlapping
@@ -90,7 +91,6 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
90
91
  };
91
92
 
92
93
  // ── structural: the query's own perceived tree ──────────────────────
93
- const starts = new Set<number>();
94
94
  starts.add(0);
95
95
  foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
96
96
  if (n.kids === null) {
@@ -115,7 +115,15 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
115
115
  leafOffsets.push(off);
116
116
  off += k.leaf?.length ?? 0;
117
117
  }
118
+ // Sub-spans starting at i > 0 begin INSIDE the chunk, at an offset the
119
+ // query's own fold did not itself choose as a boundary — the same
120
+ // opportunistic byte-atom-chain risk `tryChain`'s `boundary` gate
121
+ // guards below (see its comment). Only the chunk's own left edge
122
+ // (i === 0, already registered in `starts` above) carries the fold's
123
+ // evidence; interior sub-starts are exempt from the guard only while
124
+ // atoms themselves still discriminate at this corpus scale.
118
125
  for (let i = 0; i < n.kids.length; i++) {
126
+ if (i > 0 && atomsAreHubs) break;
119
127
  const subIds: number[] = [];
120
128
  for (let j = i; j < n.kids.length; j++) {
121
129
  const kj = n.kids[j];
@@ -158,9 +166,23 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
158
166
  }
159
167
  }
160
168
 
169
+ // A chain rebuilt from a NON-boundary offset (the query's own perceived
170
+ // cut, `starts`, never chose to segment here) is opportunistic: the same
171
+ // byte-atom coincidence the hub guard above already exists for, just
172
+ // spelled over 2+ leaves instead of 1. At small corpus scale that's fine
173
+ // — coincidence is rare and every chain is real evidence (see `atomIsHub`).
174
+ // Past the scale where atoms themselves stop discriminating, the same
175
+ // uniform-expectation argument bounds a CHAIN'S commonality too: it is at
176
+ // least as rare as its rarest atom, so a store where atoms are hubs makes
177
+ // interior chain reconstructions no more trustworthy than the atoms they
178
+ // are built from ("hi" resolving out of "W[hi]ch" is exactly this: two
179
+ // hub-scale atoms, chained at an offset nothing in the query's own fold
180
+ // selected). Chains that start ON a boundary carry the fold's own
181
+ // evidence instead and are exempt.
161
182
  const tryChain = (
162
183
  p: number,
163
184
  maxIds: number,
185
+ boundary: boolean,
164
186
  ): void => {
165
187
  const first = leafFrom(p);
166
188
  if (!first) return;
@@ -174,6 +196,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
174
196
  ids.push(nx.id);
175
197
  pos = nx.end;
176
198
  if (store.findBranch(ids) === null) continue;
199
+ if (!boundary && atomsAreHubs) continue;
177
200
  const id = resolveSpan(p, pos);
178
201
  if (id === null || id === prevId) continue;
179
202
  prevId = id;
@@ -183,10 +206,10 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
183
206
 
184
207
  for (let p = 0; p < bytes.length; p++) {
185
208
  if (starts.has(p)) {
186
- tryChain(p, chainReach(W)); // boundary start — full reach
209
+ tryChain(p, chainReach(W), true); // boundary start — full reach
187
210
  } else {
188
211
  const limit = chunkEnd[p] + W;
189
- tryChain(p, Math.min(limit - p, chainReach(W)));
212
+ tryChain(p, Math.min(limit - p, chainReach(W)), false);
190
213
  }
191
214
  }
192
215
 
@@ -211,7 +234,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
211
234
  ` (over ${leaves.length} perceived leaves)`,
212
235
  );
213
236
 
214
- return { sites, leaves, splits };
237
+ return { sites, leaves, splits, starts };
215
238
  }
216
239
 
217
240
  /** Segment bytes using the geometry's own groupings — leaf-parent
@@ -7,6 +7,7 @@
7
7
  // project) live in match.ts — the elementary match-and-project operation.
8
8
 
9
9
  import { cosine, Vec } from "../vec.js";
10
+ import { consensusFloor } from "../geometry.js";
10
11
  import type { AncestorReach, MindContext } from "./types.js";
11
12
  import { gistOf, read } from "./primitives.js";
12
13
 
@@ -527,6 +528,23 @@ export function chooseNext(
527
528
  }
528
529
  }
529
530
 
531
+ // A pick among GENUINELY competing continuations still needs to clear the
532
+ // same genuine-corroboration floor {@link consensusFloor} draws for every
533
+ // other consumer that turns distinct-context support into a vote (the
534
+ // climb's recallByResonance/commitVotes) — below it, "most corroborated"
535
+ // is only the least-thin echo among several, not real evidence. Gated to
536
+ // corpus scale large enough that this echo is even possible (the same
537
+ // scale {@link atomIsHub} already switches on for the identical reason:
538
+ // at small N every edge IS the evidence there is). A hub id has no
539
+ // meaningful "distinct context" reading at all (id < 0 atoms are excluded
540
+ // from this path already, since chooseNext only ever sees real edges).
541
+ const N = corpusN(ctx);
542
+ if (
543
+ capped.length > 1 && atomIsHub(ctx, N) && bestSupport < consensusFloor(N)
544
+ ) {
545
+ return undefined;
546
+ }
547
+
530
548
  // Trace is built lazily — the filter + map below only execute when a
531
549
  // trace listener is attached, so the common (no-trace) path pays only
532
550
  // for the prevCount calls in the loop above, never for extra rItemShort
package/src/mind/types.ts CHANGED
@@ -64,6 +64,7 @@ export interface GraphSearchHost {
64
64
  sites: ReadonlyArray<Site>;
65
65
  leaves: ReadonlyArray<Leaf>;
66
66
  splits: ReadonlySet<number>;
67
+ starts: ReadonlySet<number>;
67
68
  };
68
69
  chooseNext?(node: number): number | undefined;
69
70
  }
@@ -79,6 +80,13 @@ export interface Recognition {
79
80
  leaves: Leaf[];
80
81
  /** Sub-leaf positions where a form boundary falls between leaf edges. */
81
82
  splits: Set<number>;
83
+ /** Leaf-parent (chunk) start positions from the query's OWN perceived
84
+ * fold — the positions the fold itself chose as a grouping boundary, as
85
+ * opposed to an offset a byte-level scan merely happens to land on. The
86
+ * one boundary signal opportunistic cross-leaf recovery (recognition's
87
+ * own canonical chains, the search's `fuse`) can lean on instead of
88
+ * ASCII/word heuristics: see the `boundary` gate in recognition.ts. */
89
+ starts: Set<number>;
82
90
  }
83
91
 
84
92
  /** How the consensus climb weights a region's Document-Frequency reach. */
@@ -93,6 +101,16 @@ export interface Attention {
93
101
  /** The union of the query byte-spans whose evidence supports this point. */
94
102
  start: number;
95
103
  end: number;
104
+ /** SCALE-INVARIANT confidence: the fraction of the query's OWN regions
105
+ * whose evidence this point accounts for (Σ RegionVote.absorbed among
106
+ * its contributors, over the query's total region count) — read PER-
107
+ * ANCHOR, unlike the raw IDF vote (an absolute, ln(N)-scaled quantity
108
+ * that means "strong" on a small store and "weak" on a large one for
109
+ * the SAME degree of genuine consensus). A point whose breadth clears
110
+ * `dominates` (> half the query's regions corroborate it) is real
111
+ * consensus; one that does not is a coincidental single-region echo —
112
+ * see test/35-attention-confidence.test.mjs. */
113
+ breadth: number;
96
114
  }
97
115
 
98
116
  /** Both read-outs of one consensus climb. */
@@ -130,6 +148,14 @@ export interface RegionVote {
130
148
  roots: readonly number[];
131
149
  w: number;
132
150
  wFocus: number;
151
+ /** How many of the query's ORIGINAL regions this one vote's evidence
152
+ * accounts for. 1 for an ordinary per-region vote (itself); for a
153
+ * cross-region junction vote, 1 (itself) plus however many individual
154
+ * votes it explained away (see crossRegionVotes) — the junction speaks
155
+ * for all of them at once, and breadth accounting must not undercount it
156
+ * to "one region" just because it collapsed to one pooled axiom.
157
+ * Defaults to 1 when absent. */
158
+ absorbed?: number;
133
159
  }
134
160
 
135
161
  /** The edge-bearing contexts reached by climbing from a node, plus saturation info. */
@@ -256,6 +282,20 @@ export function liftAnswer(segs: Seg[], queryLen: number): Uint8Array | null {
256
282
 
257
283
  if (recognised.length === 1) {
258
284
  const s = segs[recognised[0]];
285
+ // A COMPUTED span's query-side width is operand digit-count, not
286
+ // evidence of how much of the query's meaning it accounts for — the
287
+ // half-dominance check below (built for a genuinely RECOGNISED learned
288
+ // form) is not a valid framing signal for it (see the `computed` field
289
+ // doc on Seg/GItem): "1000 - 421" outweighs "what is …?" by width only
290
+ // because the operands are big, not because the framing matters less.
291
+ // A LITERAL PREFIX before a computed span is unambiguous framing
292
+ // regardless of width — an arithmetic expression is never itself
293
+ // preceded by more literal computed content, so anything literal before
294
+ // it is question wording ("what is ", "compute ") to lift clear of.
295
+ // With no prefix (s.i === 0) the span is judged by the ordinary
296
+ // half-dominance rule below, which already correctly keeps a short
297
+ // trailing glue byte ("2+2." → "4.", the span dominates a 4-byte query).
298
+ if (s.computed && s.i > 0) return s.bytes;
259
299
  if (dominates(s.j - s.i, queryLen)) {
260
300
  return concatBytes(segs.map((x) => x.bytes));
261
301
  }
@@ -0,0 +1,139 @@
1
+ // 35-attention-confidence.test.mjs — SCALE-INVARIANT CONFIDENCE for a point
2
+ // of attention (Attention.breadth).
3
+ //
4
+ // commitVotes always admits the DOMINANT root regardless of its IDF-weighted
5
+ // vote (attention.ts: "roots.length === 0 || ..." — the first root is never
6
+ // floor-gated). That is correct for the common case ("give me your best
7
+ // guess"), but it means a query's SOLE root can be either:
8
+ //
9
+ // (a) genuine consensus — most of the query's own regions independently
10
+ // corroborate it (a real fact, or a real cross-region binding), or
11
+ // (b) a coincidental echo — ONE region's resonance happened to land
12
+ // somewhere, with the rest of the query silent on it.
13
+ //
14
+ // The raw IDF vote cannot tell these apart: it is an ABSOLUTE quantity that
15
+ // scales with ln(corpus size), so the same vote means "strong" on a small
16
+ // store and "weak" on a large one (see the session's earlier finding: a
17
+ // genuine root on a 325K-context store scored BELOW its own consensus floor,
18
+ // while a spurious echo on a 15-fact store scored comfortably above its own —
19
+ // much smaller — floor). A SCALE-INVARIANT measure is needed instead: what
20
+ // FRACTION of the query's own regions this root's evidence accounts for —
21
+ // the "N of M sub-regions voted" the rationale already reports, but read
22
+ // PER-ANCHOR instead of globally, and tested against the same half-dominance
23
+ // convention (`dominates`, part*2 > whole) the rest of the codebase already
24
+ // uses for every other "is this real signal or noise" decision.
25
+
26
+ import { test } from "node:test";
27
+ import assert from "node:assert/strict";
28
+ import { Mind } from "../dist/src/index.js";
29
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
30
+
31
+ const enc = (s) => new TextEncoder().encode(s);
32
+ const mk = (seed) =>
33
+ new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
34
+
35
+ // ═══════════════════════════════════════════════════════════════════════════
36
+ // GENUINE CONSENSUS — a real cross-region binding (test/34's own corpus).
37
+ // Most of the query's regions agree on the joint context; breadth must
38
+ // DOMINATE (> half of the query's own regions corroborate it).
39
+ // ═══════════════════════════════════════════════════════════════════════════
40
+
41
+ const BINDING_CORPUS = [
42
+ ["red", "is a color"],
43
+ ["blue", "is a color"],
44
+ ["circle", "is a shape"],
45
+ ["square", "is a shape"],
46
+ ["red circle", "answer alpha"],
47
+ ["red square", "answer beta"],
48
+ ["blue circle", "answer gamma"],
49
+ ["blue square", "answer delta"],
50
+ ];
51
+
52
+ test("breadth: a genuine cross-region binding dominates the query's regions", async () => {
53
+ const m = mk(1);
54
+ await m.ingest(BINDING_CORPUS);
55
+ const roots = await m.climbAttention(enc("red then circle"), 24);
56
+ assert.equal(roots.length, 1, "expected exactly one committed root");
57
+ assert.ok(
58
+ typeof roots[0].breadth === "number",
59
+ "Attention must carry a breadth field",
60
+ );
61
+ assert.ok(
62
+ roots[0].breadth > 0.5,
63
+ `genuine binding must dominate (> half the query's own regions), got breadth=${
64
+ roots[0].breadth
65
+ }`,
66
+ );
67
+ await m.store.close();
68
+ });
69
+
70
+ // ═══════════════════════════════════════════════════════════════════════════
71
+ // SPURIOUS ECHO — a short arithmetic query whose consensus climb lands on an
72
+ // UNRELATED fact by coincidental byte-pattern resonance (observed directly
73
+ // this session: "2+2 equals what?" climbs to "1+1", not "2+2"). Only a
74
+ // minority of the query's regions support it; breadth must NOT dominate.
75
+ // ═══════════════════════════════════════════════════════════════════════════
76
+
77
+ const ARITH_CORPUS = [
78
+ ["1+2", "3"],
79
+ ["2+2", "4"],
80
+ ["2+3", "5"],
81
+ ["3+3", "6"],
82
+ ["3+5", "8"],
83
+ ["4+3", "7"],
84
+ ["2+5", "7"],
85
+ ["1+5", "6"],
86
+ ["6+1", "7"],
87
+ ["4+1", "5"],
88
+ ["3+4", "7"],
89
+ ["5+2", "7"],
90
+ ["1+1", "2"],
91
+ ["5+3", "8"],
92
+ ["7+1", "8"],
93
+ ];
94
+
95
+ test("breadth: a coincidental single-region echo does not dominate", async () => {
96
+ const m = mk(1);
97
+ await m.ingest(ARITH_CORPUS);
98
+ const roots = await m.climbAttention(enc("2+2 equals what?"), 24);
99
+ assert.equal(roots.length, 1, "expected exactly one committed root");
100
+ assert.ok(
101
+ typeof roots[0].breadth === "number",
102
+ "Attention must carry a breadth field",
103
+ );
104
+ assert.ok(
105
+ roots[0].breadth <= 0.5,
106
+ `a coincidental echo must NOT dominate the query's own regions, got breadth=${
107
+ roots[0].breadth
108
+ }`,
109
+ );
110
+ await m.store.close();
111
+ });
112
+
113
+ // ═══════════════════════════════════════════════════════════════════════════
114
+ // SCALE INVARIANCE — the whole point: the SAME breadth bar must separate
115
+ // signal from noise whether the corpus has 15 facts or many more, unlike the
116
+ // raw IDF vote (which is an absolute, ln(N)-scaled quantity — see the header
117
+ // comment). Doubling the corpus (more unrelated arithmetic facts) must not
118
+ // flip either verdict merely by changing N.
119
+ // ═══════════════════════════════════════════════════════════════════════════
120
+
121
+ test("breadth: the same bar holds as the corpus grows (scale invariance)", async () => {
122
+ const bigArith = [...ARITH_CORPUS];
123
+ for (let a = 1; a <= 9; a++) {
124
+ for (let b = 1; b <= 9; b++) {
125
+ bigArith.push([`${a}x${b}`, String(a * b)]);
126
+ }
127
+ }
128
+ const m = mk(1);
129
+ await m.ingest(bigArith);
130
+ const roots = await m.climbAttention(enc("2+2 equals what?"), 24);
131
+ assert.equal(roots.length, 1, "expected exactly one committed root");
132
+ assert.ok(
133
+ roots[0].breadth <= 0.5,
134
+ `a coincidental echo must still not dominate on a larger corpus, got breadth=${
135
+ roots[0].breadth
136
+ }`,
137
+ );
138
+ await m.store.close();
139
+ });