@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.
- package/dist/src/mind/articulation.js +1 -1
- package/dist/src/mind/attention.d.ts +4 -0
- package/dist/src/mind/attention.js +36 -12
- package/dist/src/mind/graph-search.d.ts +16 -1
- package/dist/src/mind/graph-search.js +34 -5
- package/dist/src/mind/mechanisms/alu.js +8 -1
- package/dist/src/mind/mechanisms/cast.js +35 -11
- package/dist/src/mind/mechanisms/cover.js +8 -1
- package/dist/src/mind/mechanisms/extraction.js +75 -30
- package/dist/src/mind/mind.d.ts +1 -0
- package/dist/src/mind/mind.js +6 -1
- package/dist/src/mind/pipeline.js +34 -2
- package/dist/src/mind/reasoning.d.ts +9 -1
- package/dist/src/mind/reasoning.js +26 -4
- package/dist/src/mind/recognition.js +30 -6
- package/dist/src/mind/traverse.js +15 -0
- package/dist/src/mind/types.d.ts +26 -0
- package/dist/src/mind/types.js +15 -0
- package/package.json +1 -1
- package/src/mind/articulation.ts +1 -0
- package/src/mind/attention.ts +42 -12
- package/src/mind/graph-search.ts +59 -2
- package/src/mind/mechanisms/alu.ts +8 -1
- package/src/mind/mechanisms/cast.ts +46 -8
- package/src/mind/mechanisms/cover.ts +8 -0
- package/src/mind/mechanisms/extraction.ts +101 -40
- package/src/mind/mind.ts +7 -1
- package/src/mind/pipeline.ts +37 -2
- package/src/mind/reasoning.ts +26 -3
- package/src/mind/recognition.ts +28 -5
- package/src/mind/traverse.ts +18 -0
- package/src/mind/types.ts +40 -0
- package/test/35-attention-confidence.test.mjs +139 -0
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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 (
|
|
152
|
+
if (shapeMisses === searched.length) {
|
|
86
153
|
ctx.trace?.step(
|
|
87
154
|
"trySkillAnchors",
|
|
88
155
|
[],
|
|
89
156
|
[],
|
|
90
|
-
`none of ${
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
278
|
+
return null;
|
|
204
279
|
}
|
|
205
280
|
|
|
206
281
|
const out = pieces.length === 1 ? pieces[0] : concatBytes(pieces);
|
|
207
|
-
|
|
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 {
|
|
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.
|
package/src/mind/pipeline.ts
CHANGED
|
@@ -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
|
-
|
|
292
|
-
|
|
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(
|
package/src/mind/reasoning.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
...
|
|
189
|
+
...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
|
|
167
190
|
]);
|
|
168
|
-
for (const root of
|
|
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;
|
package/src/mind/recognition.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/mind/traverse.ts
CHANGED
|
@@ -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
|
+
});
|