@hviana/sema 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/geometry.d.ts +28 -0
- package/dist/src/geometry.js +35 -0
- package/dist/src/mind/articulation.js +1 -1
- package/dist/src/mind/attention.d.ts +4 -0
- package/dist/src/mind/attention.js +65 -12
- package/dist/src/mind/graph-search.d.ts +16 -1
- package/dist/src/mind/graph-search.js +34 -5
- package/dist/src/mind/learning.d.ts +1 -1
- package/dist/src/mind/learning.js +20 -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 +40 -1
- package/dist/src/mind/mechanisms/extraction.js +75 -30
- package/dist/src/mind/mechanisms/recall.js +26 -2
- package/dist/src/mind/mind.d.ts +3 -2
- package/dist/src/mind/mind.js +28 -54
- package/dist/src/mind/pipeline.js +34 -2
- package/dist/src/mind/primitives.d.ts +16 -9
- package/dist/src/mind/primitives.js +58 -22
- 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 +58 -10
- package/dist/src/mind/types.js +15 -0
- package/package.json +1 -1
- package/src/geometry.ts +61 -1
- package/src/mind/articulation.ts +1 -0
- package/src/mind/attention.ts +80 -12
- package/src/mind/graph-search.ts +59 -2
- package/src/mind/learning.ts +20 -3
- package/src/mind/mechanisms/alu.ts +8 -1
- package/src/mind/mechanisms/cast.ts +46 -8
- package/src/mind/mechanisms/cover.ts +46 -0
- package/src/mind/mechanisms/extraction.ts +101 -40
- package/src/mind/mechanisms/recall.ts +30 -2
- package/src/mind/mind.ts +38 -61
- package/src/mind/pipeline.ts +37 -2
- package/src/mind/primitives.ts +71 -29
- 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 +73 -10
- package/test/35-attention-confidence.test.mjs +139 -0
package/src/mind/mind.ts
CHANGED
|
@@ -14,8 +14,6 @@ import { bindSeat, fold, Sema, Space } from "../sema.js";
|
|
|
14
14
|
import { Alphabet } from "../alphabet.js";
|
|
15
15
|
import {
|
|
16
16
|
bytesToTree,
|
|
17
|
-
bytesToTreePyramid,
|
|
18
|
-
type FoldPyramid,
|
|
19
17
|
Grid,
|
|
20
18
|
gridToTree,
|
|
21
19
|
hilbertBytes,
|
|
@@ -109,7 +107,7 @@ export interface Conversation {
|
|
|
109
107
|
* foldTree returns their ids immediately — O(suffix) instead of
|
|
110
108
|
* O(context) for every tree walk. */
|
|
111
109
|
interface ConversationData {
|
|
112
|
-
|
|
110
|
+
tree: Sema;
|
|
113
111
|
bytes: Uint8Array;
|
|
114
112
|
boundaries: number[];
|
|
115
113
|
perceiveMemo: Map<string, Sema>;
|
|
@@ -244,9 +242,10 @@ export class Mind implements MindContext {
|
|
|
244
242
|
// bounded: a pyramid costs ~KB per content byte (one D-float gist per
|
|
245
243
|
// interior node), and only the few live conversation chains need to stay
|
|
246
244
|
// warm, so 8 entries is the honest budget.
|
|
247
|
-
_depositTrees = new BoundedMap<
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
_depositTrees = new BoundedMap<
|
|
246
|
+
string,
|
|
247
|
+
import("./types.js").DepositCacheEntry
|
|
248
|
+
>(8);
|
|
250
249
|
_depositLens = new Set<number>();
|
|
251
250
|
_internIds = new WeakMap<import("../sema.js").Sema, number>();
|
|
252
251
|
|
|
@@ -267,9 +266,15 @@ export class Mind implements MindContext {
|
|
|
267
266
|
sites: ReadonlyArray<Site>;
|
|
268
267
|
leaves: ReadonlyArray<Leaf>;
|
|
269
268
|
splits: ReadonlySet<number>;
|
|
269
|
+
starts: ReadonlySet<number>;
|
|
270
270
|
} {
|
|
271
271
|
const r = recognise(this, bytes);
|
|
272
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
sites: r.sites,
|
|
274
|
+
leaves: r.leaves,
|
|
275
|
+
splits: r.splits,
|
|
276
|
+
starts: r.starts,
|
|
277
|
+
};
|
|
273
278
|
}
|
|
274
279
|
|
|
275
280
|
/** Disambiguate among multiple learnt continuations of the same context node.
|
|
@@ -513,18 +518,20 @@ export class Mind implements MindContext {
|
|
|
513
518
|
* where one turn ends and the next begins. */
|
|
514
519
|
beginConversation(state?: ConversationState): Conversation {
|
|
515
520
|
const id = this._nextConvId++;
|
|
516
|
-
// Build the initial pyramid: from scratch for a new conversation,
|
|
517
|
-
// or from the saved context bytes when restoring.
|
|
518
521
|
const initBytes = state?.context ?? new Uint8Array(0);
|
|
519
|
-
const
|
|
522
|
+
const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
|
|
523
|
+
const tree = bytesToTree(
|
|
520
524
|
this.space,
|
|
521
525
|
this.alphabet,
|
|
522
526
|
initBytes,
|
|
527
|
+
undefined,
|
|
528
|
+
undefined,
|
|
529
|
+
initBoundaries.length > 0 ? initBoundaries : undefined,
|
|
523
530
|
);
|
|
524
531
|
this._conversations.set(id, {
|
|
525
|
-
|
|
532
|
+
tree,
|
|
526
533
|
bytes: initBytes,
|
|
527
|
-
boundaries:
|
|
534
|
+
boundaries: initBoundaries,
|
|
528
535
|
perceiveMemo: new Map(),
|
|
529
536
|
recogniseMemo: new Map(),
|
|
530
537
|
climbMemo: new Map(),
|
|
@@ -572,25 +579,23 @@ export class Mind implements MindContext {
|
|
|
572
579
|
* place a context grows ({@link addTurn} and {@link respondTurn} both
|
|
573
580
|
* come through here), so the append semantics cannot drift. */
|
|
574
581
|
private _growContext(data: ConversationData, turnBytes: Uint8Array): Sema {
|
|
575
|
-
const prevLen = data.
|
|
582
|
+
const prevLen = data.bytes.length;
|
|
576
583
|
// An empty turn neither grows the context nor marks a boundary —
|
|
577
584
|
// boundaries are documented strictly increasing, and a zero-length
|
|
578
|
-
// "turn" is no turn.
|
|
579
|
-
// refold below O(1), so the existing tree is returned unchanged.
|
|
585
|
+
// "turn" is no turn. Nothing changed, so the existing tree stands.
|
|
580
586
|
const grow = turnBytes.length > 0;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
: turnBytes;
|
|
586
|
-
if (grow && prevLen > 0) data.boundaries.push(prevLen);
|
|
587
|
-
const { tree, pyramid } = bytesToTreePyramid(
|
|
587
|
+
if (!grow) return data.tree;
|
|
588
|
+
const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
|
|
589
|
+
if (prevLen > 0) data.boundaries.push(prevLen);
|
|
590
|
+
const tree = bytesToTree(
|
|
588
591
|
this.space,
|
|
589
592
|
this.alphabet,
|
|
590
593
|
grown,
|
|
591
|
-
|
|
594
|
+
undefined,
|
|
595
|
+
undefined,
|
|
596
|
+
data.boundaries.length > 0 ? data.boundaries : undefined,
|
|
592
597
|
);
|
|
593
|
-
data.
|
|
598
|
+
data.tree = tree;
|
|
594
599
|
data.bytes = grown;
|
|
595
600
|
data.perceiveMemo.set(latin1Key(grown), tree);
|
|
596
601
|
return tree;
|
|
@@ -644,43 +649,15 @@ export class Mind implements MindContext {
|
|
|
644
649
|
this.canonMemo = this.canon ? new Map() : null;
|
|
645
650
|
|
|
646
651
|
try {
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
const suffixStart = data.boundaries[data.boundaries.length - 1];
|
|
657
|
-
if (suffixStart < newContext.length) {
|
|
658
|
-
const suffixBytes = newContext.subarray(suffixStart);
|
|
659
|
-
const suffixRec = recognise(this, suffixBytes);
|
|
660
|
-
const fullRec = recognise(this, newContext);
|
|
661
|
-
const seen = new Set(fullRec.sites.map((s) => `${s.start},${s.end}`));
|
|
662
|
-
const mergedSites = [...fullRec.sites];
|
|
663
|
-
for (const site of suffixRec.sites) {
|
|
664
|
-
const absStart = suffixStart + site.start;
|
|
665
|
-
const absEnd = suffixStart + site.end;
|
|
666
|
-
const key = `${absStart},${absEnd}`;
|
|
667
|
-
if (!seen.has(key)) {
|
|
668
|
-
seen.add(key);
|
|
669
|
-
mergedSites.push({
|
|
670
|
-
start: absStart,
|
|
671
|
-
end: absEnd,
|
|
672
|
-
payload: site.payload,
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
data.recogniseMemo.set(latin1Key(newContext), {
|
|
677
|
-
sites: mergedSites,
|
|
678
|
-
leaves: fullRec.leaves,
|
|
679
|
-
splits: fullRec.splits,
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
652
|
+
// No recognise-memo pre-seeding here: that used to be necessary
|
|
653
|
+
// because the flat/positional fold lost visibility into an earlier
|
|
654
|
+
// turn's own structure once later bytes shifted its position
|
|
655
|
+
// (foldTree no longer visited the turn's root node). The
|
|
656
|
+
// STABLE-PREFIX fold (see {@link ConversationData}) makes every
|
|
657
|
+
// turn's subtree independent of what follows it by construction, so
|
|
658
|
+
// recognise() finds it correctly on its own, first-touch, exactly
|
|
659
|
+
// once per turn — no workaround needed, and none pre-empting
|
|
660
|
+
// recognise()'s own memo with a partial result.
|
|
684
661
|
const top = this.trace?.enter("respondTurn", [
|
|
685
662
|
rItem(newContext, "query"),
|
|
686
663
|
]);
|
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/primitives.ts
CHANGED
|
@@ -8,16 +8,16 @@ import { Sema } from "../sema.js";
|
|
|
8
8
|
import {
|
|
9
9
|
bytesToTree,
|
|
10
10
|
bytesToTreePyramid,
|
|
11
|
-
type FoldPyramid,
|
|
12
11
|
Grid,
|
|
13
12
|
gridToTree,
|
|
14
13
|
hilbertBytes,
|
|
14
|
+
stablePrefixFoldIncremental,
|
|
15
15
|
stackGrids,
|
|
16
16
|
} from "../geometry.js";
|
|
17
17
|
import { canonHash } from "../canon.js";
|
|
18
18
|
import { bytesEqual } from "../bytes.js";
|
|
19
19
|
import { ALL } from "./types.js";
|
|
20
|
-
import type { Input, MindContext } from "./types.js";
|
|
20
|
+
import type { DepositCacheEntry, Input, MindContext } from "./types.js";
|
|
21
21
|
|
|
22
22
|
// ── Address: bytes → node ──────────────────────────────────────────────
|
|
23
23
|
|
|
@@ -93,34 +93,76 @@ export function perceive(
|
|
|
93
93
|
return gridToTree(ctx.space, ctx.alphabet, input as Grid);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** The DEPOSIT-shaped perceive
|
|
97
|
-
* perception
|
|
98
|
-
* for exact recall),
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
96
|
+
/** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
|
|
97
|
+
* (bit-identical to inference perception of a standalone query — that
|
|
98
|
+
* structural train/inference agreement is load-bearing for exact recall),
|
|
99
|
+
* computed incrementally via the fold's level pyramid
|
|
100
|
+
* ({@link bytesToTreePyramid}). An input that EXTENDS a previously
|
|
101
|
+
* deposited one is a conversation context grown by one turn — the cached
|
|
102
|
+
* prefix length IS the turn boundary (derived from the deposit sequence
|
|
103
|
+
* itself, never from content conventions) — and takes the STABLE-PREFIX
|
|
104
|
+
* fold over the accumulated boundaries, bit-identical to the boundary
|
|
105
|
+
* fold query-time conversation perception uses, so the trained context
|
|
106
|
+
* node and the query's context subtree are the SAME node. Segment folds
|
|
107
|
+
* reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
|
|
108
|
+
* instead of O(context) per turn. The fold state is purely a cache; the
|
|
109
|
+
* boundary accumulation is what an evicted chain loses (falling back to
|
|
110
|
+
* the plain fold, the pre-boundary shape — a warm replay restores it). */
|
|
111
|
+
export function perceiveDeposit(
|
|
112
|
+
ctx: MindContext,
|
|
113
|
+
bytes: Uint8Array,
|
|
114
|
+
conversational = false,
|
|
115
|
+
): Sema {
|
|
116
|
+
let prev: DepositCacheEntry | undefined;
|
|
117
|
+
let prefixLen = 0;
|
|
118
|
+
// Cache consult (both boundary lookup and stable-prefix reuse) is scoped
|
|
119
|
+
// to conversational deposits only — a bare, unrelated fact whose bytes
|
|
120
|
+
// happen to extend an earlier deposit is NOT a conversation turn, and
|
|
121
|
+
// must keep the plain fold so it shares structure with ITS OWN prior
|
|
122
|
+
// deposits, not fragment against a coincidental byte-prefix.
|
|
123
|
+
if (conversational) {
|
|
124
|
+
// Longest cached PROPER prefix first.
|
|
125
|
+
const lens = [...ctx._depositLens]
|
|
126
|
+
.filter((L) => L >= 2 && L < bytes.length)
|
|
127
|
+
.sort((a, b) => b - a);
|
|
128
|
+
for (const L of lens) {
|
|
129
|
+
const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
|
|
130
|
+
// The suffix must bytes-equal the hit's OWN recorded continuation —
|
|
131
|
+
// proof this deposit is that turn's actual next turn, not a fact
|
|
132
|
+
// that coincidentally shares its byte prefix.
|
|
133
|
+
if (
|
|
134
|
+
hit !== undefined && hit.nextBytes !== undefined &&
|
|
135
|
+
bytesEqual(hit.nextBytes, bytes.subarray(L))
|
|
136
|
+
) {
|
|
137
|
+
prev = hit;
|
|
138
|
+
prefixLen = L;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
115
141
|
}
|
|
116
142
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
let tree: Sema;
|
|
144
|
+
let entry: DepositCacheEntry;
|
|
145
|
+
if (prev !== undefined) {
|
|
146
|
+
const boundaries = [...prev.boundaries, prefixLen];
|
|
147
|
+
const folded = stablePrefixFoldIncremental(
|
|
148
|
+
ctx.space,
|
|
149
|
+
ctx.alphabet,
|
|
150
|
+
bytes,
|
|
151
|
+
boundaries,
|
|
152
|
+
prev.stable,
|
|
153
|
+
);
|
|
154
|
+
tree = folded.tree;
|
|
155
|
+
entry = { boundaries, stable: folded.fold };
|
|
156
|
+
} else {
|
|
157
|
+
const plain = bytesToTreePyramid(ctx.space, ctx.alphabet, bytes);
|
|
158
|
+
tree = plain.tree;
|
|
159
|
+
entry = { boundaries: [], pyramid: plain.pyramid };
|
|
160
|
+
}
|
|
161
|
+
// Only a conversational deposit writes the cache too — otherwise a bare
|
|
162
|
+
// fact's plain fold could later be misread as a conversation's turn-zero
|
|
163
|
+
// boundary by an unrelated conversational deposit that happens to extend
|
|
164
|
+
// its bytes.
|
|
165
|
+
if (conversational && bytes.length >= 2) {
|
|
124
166
|
// The lengths set drifts as the map evicts; past the probe budget the
|
|
125
167
|
// drift itself becomes the cost (each stale length is an O(len) key
|
|
126
168
|
// build), so both reset together — losing only warm-up on live chains.
|
|
@@ -128,7 +170,7 @@ export function perceiveDeposit(ctx: MindContext, bytes: Uint8Array): Sema {
|
|
|
128
170
|
ctx._depositLens.clear();
|
|
129
171
|
ctx._depositTrees.clear();
|
|
130
172
|
}
|
|
131
|
-
ctx._depositTrees.set(latin1Key(bytes),
|
|
173
|
+
ctx._depositTrees.set(latin1Key(bytes), entry);
|
|
132
174
|
ctx._depositLens.add(bytes.length);
|
|
133
175
|
}
|
|
134
176
|
return tree;
|
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
|
@@ -19,7 +19,26 @@ import type {
|
|
|
19
19
|
Site,
|
|
20
20
|
} from "./graph-search.js";
|
|
21
21
|
import type { Rationale } from "./rationale.js";
|
|
22
|
-
import type { FoldPyramid, Grid } from "../geometry.js";
|
|
22
|
+
import type { FoldPyramid, Grid, StableFold } from "../geometry.js";
|
|
23
|
+
|
|
24
|
+
/** One {@link MindContext._depositTrees} entry — see that field's doc. */
|
|
25
|
+
export interface DepositCacheEntry {
|
|
26
|
+
/** Turn boundaries accumulated over this content's deposit chain —
|
|
27
|
+
* strictly increasing proper offsets, each a previously-deposited
|
|
28
|
+
* whole-context length. Empty for a first-seen (single-turn) input. */
|
|
29
|
+
boundaries: number[];
|
|
30
|
+
/** Plain-fold pyramid (first-seen inputs only). */
|
|
31
|
+
pyramid?: FoldPyramid;
|
|
32
|
+
/** Stable-prefix segment folds (grown-context inputs only). */
|
|
33
|
+
stable?: StableFold;
|
|
34
|
+
/** The continuation bytes this ctxInput was paired with in ingestPair, if
|
|
35
|
+
* any — the ONLY thing that makes a later, longer ctxInput a genuine next
|
|
36
|
+
* TURN of the same conversation rather than an unrelated fact that
|
|
37
|
+
* happens to share this one's byte prefix (e.g. "2+2" vs. "2+2=5"). A
|
|
38
|
+
* later deposit only takes this entry as its stable-prefix `prev` when
|
|
39
|
+
* its own suffix bytes-equal this exactly. */
|
|
40
|
+
nextBytes?: Uint8Array;
|
|
41
|
+
}
|
|
23
42
|
import { concatBytes } from "../bytes.js";
|
|
24
43
|
import { dominates } from "../geometry.js";
|
|
25
44
|
|
|
@@ -45,6 +64,7 @@ export interface GraphSearchHost {
|
|
|
45
64
|
sites: ReadonlyArray<Site>;
|
|
46
65
|
leaves: ReadonlyArray<Leaf>;
|
|
47
66
|
splits: ReadonlySet<number>;
|
|
67
|
+
starts: ReadonlySet<number>;
|
|
48
68
|
};
|
|
49
69
|
chooseNext?(node: number): number | undefined;
|
|
50
70
|
}
|
|
@@ -60,6 +80,13 @@ export interface Recognition {
|
|
|
60
80
|
leaves: Leaf[];
|
|
61
81
|
/** Sub-leaf positions where a form boundary falls between leaf edges. */
|
|
62
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>;
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
/** How the consensus climb weights a region's Document-Frequency reach. */
|
|
@@ -74,6 +101,16 @@ export interface Attention {
|
|
|
74
101
|
/** The union of the query byte-spans whose evidence supports this point. */
|
|
75
102
|
start: number;
|
|
76
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;
|
|
77
114
|
}
|
|
78
115
|
|
|
79
116
|
/** Both read-outs of one consensus climb. */
|
|
@@ -111,6 +148,14 @@ export interface RegionVote {
|
|
|
111
148
|
roots: readonly number[];
|
|
112
149
|
w: number;
|
|
113
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;
|
|
114
159
|
}
|
|
115
160
|
|
|
116
161
|
/** The edge-bearing contexts reached by climbing from a node, plus saturation info. */
|
|
@@ -189,15 +234,19 @@ export interface MindContext extends GraphSearchHost {
|
|
|
189
234
|
* never invalidated. Bounded LRU (byte-sized); a miss only re-perceives,
|
|
190
235
|
* never a correctness risk. */
|
|
191
236
|
_gistCache: BoundedMap<number, Vec>;
|
|
192
|
-
/** DEPOSIT-path
|
|
193
|
-
*
|
|
194
|
-
* whose
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
|
|
237
|
+
/** DEPOSIT-path perception cache: content key (latin1) of a deposited
|
|
238
|
+
* input → its accumulated turn BOUNDARIES plus reusable fold state. A
|
|
239
|
+
* deposit whose content extends a cached entry IS a conversation context
|
|
240
|
+
* grown by one turn — the cached length is the new boundary — so it
|
|
241
|
+
* folds with the SAME stable-prefix fold query-time perception uses
|
|
242
|
+
* (structural train/inference agreement, load-bearing for recall),
|
|
243
|
+
* reusing every already-folded segment via `stable` (see StableFold) —
|
|
244
|
+
* O(turn) per deposit instead of O(context). A first-seen input keeps
|
|
245
|
+
* the plain fold and caches its `pyramid` (see FoldPyramid). Purely a
|
|
246
|
+
* performance cache for the FOLD STATE; the boundaries are semantic but
|
|
247
|
+
* derived only from the deposit sequence itself (an evicted chain falls
|
|
248
|
+
* back to plain-fold behavior, exactly the pre-boundary shape). */
|
|
249
|
+
_depositTrees: BoundedMap<string, DepositCacheEntry>;
|
|
201
250
|
/** The byte lengths present in {@link _depositTrees} — the candidate
|
|
202
251
|
* prefix lengths probed (longest first). Drifts on eviction (a stale
|
|
203
252
|
* length only costs a miss); cleared with the map when it outgrows the
|
|
@@ -233,6 +282,20 @@ export function liftAnswer(segs: Seg[], queryLen: number): Uint8Array | null {
|
|
|
233
282
|
|
|
234
283
|
if (recognised.length === 1) {
|
|
235
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;
|
|
236
299
|
if (dominates(s.j - s.i, queryLen)) {
|
|
237
300
|
return concatBytes(segs.map((x) => x.bytes));
|
|
238
301
|
}
|