@hviana/sema 0.1.6 → 0.1.8
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/example/train_base.js +42 -9
- package/dist/src/alu/src/parser.d.ts +9 -0
- package/dist/src/alu/src/parser.js +110 -2
- package/dist/src/canon.d.ts +26 -0
- package/dist/src/canon.js +57 -0
- package/dist/src/geometry.d.ts +13 -2
- package/dist/src/geometry.js +101 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/mind/attention.js +11 -7
- package/dist/src/mind/learning.js +33 -1
- package/dist/src/mind/match.d.ts +4 -2
- package/dist/src/mind/match.js +30 -6
- package/dist/src/mind/mechanisms/cast.js +18 -4
- package/dist/src/mind/mechanisms/confluence.js +13 -1
- package/dist/src/mind/mechanisms/recall.js +115 -31
- package/dist/src/mind/mind.d.ts +138 -12
- package/dist/src/mind/mind.js +284 -22
- package/dist/src/mind/primitives.d.ts +22 -2
- package/dist/src/mind/primitives.js +95 -6
- package/dist/src/mind/recognition.js +31 -8
- package/dist/src/mind/traverse.d.ts +13 -0
- package/dist/src/mind/traverse.js +41 -0
- package/dist/src/mind/types.d.ts +33 -21
- package/dist/src/store-sqlite.d.ts +10 -0
- package/dist/src/store-sqlite.js +58 -0
- package/dist/src/store.d.ts +23 -0
- package/dist/src/store.js +13 -2
- package/example/train_base.ts +49 -9
- package/index.html +65 -0
- package/package.json +1 -1
- package/src/alu/src/parser.ts +105 -4
- package/src/canon.ts +65 -0
- package/src/geometry.ts +105 -1
- package/src/index.ts +1 -0
- package/src/mind/attention.ts +11 -6
- package/src/mind/learning.ts +39 -1
- package/src/mind/match.ts +29 -6
- package/src/mind/mechanisms/cast.ts +21 -6
- package/src/mind/mechanisms/confluence.ts +15 -1
- package/src/mind/mechanisms/recall.ts +132 -41
- package/src/mind/mind.ts +396 -22
- package/src/mind/primitives.ts +109 -7
- package/src/mind/recognition.ts +36 -8
- package/src/mind/traverse.ts +47 -0
- package/src/mind/types.ts +30 -21
- package/src/store-sqlite.ts +76 -0
- package/src/store.ts +46 -2
- package/test/13-conversation.test.mjs +240 -20
- package/test/35-prefix-edge.test.mjs +86 -0
package/src/mind/primitives.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
// Read — node → bytes (read)
|
|
5
5
|
|
|
6
6
|
import { Vec } from "../vec.js";
|
|
7
|
-
import { Sema
|
|
8
|
-
import { Alphabet } from "../alphabet.js";
|
|
7
|
+
import { Sema } from "../sema.js";
|
|
9
8
|
import {
|
|
10
9
|
bytesToTree,
|
|
11
10
|
bytesToTreePyramid,
|
|
@@ -15,6 +14,8 @@ import {
|
|
|
15
14
|
hilbertBytes,
|
|
16
15
|
stackGrids,
|
|
17
16
|
} from "../geometry.js";
|
|
17
|
+
import { canonHash } from "../canon.js";
|
|
18
|
+
import { bytesEqual } from "../bytes.js";
|
|
18
19
|
import { ALL } from "./types.js";
|
|
19
20
|
import type { Input, MindContext } from "./types.js";
|
|
20
21
|
|
|
@@ -24,7 +25,7 @@ import type { Input, MindContext } from "./types.js";
|
|
|
24
25
|
* collision-free encoding. Spans on the perception path are query-scale
|
|
25
26
|
* (windows, regions, candidate spans), so key construction is far cheaper
|
|
26
27
|
* than the river fold it deduplicates. */
|
|
27
|
-
function latin1Key(bytes: Uint8Array): string {
|
|
28
|
+
export function latin1Key(bytes: Uint8Array): string {
|
|
28
29
|
// Batched String.fromCharCode — avoids the O(n²) cost of repeated += on
|
|
29
30
|
// potentially-large query spans, and stays well under the ~65536 arg limit.
|
|
30
31
|
const n = bytes.length;
|
|
@@ -36,12 +37,19 @@ function latin1Key(bytes: Uint8Array): string {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** Perceive input into a content-defined tree (the river fold).
|
|
39
|
-
* Deterministic — identical bytes always produce an identical tree.
|
|
40
|
+
* Deterministic — identical bytes always produce an identical tree.
|
|
41
|
+
*
|
|
42
|
+
* `boundaries` is an optional sorted list of proper byte offsets where the
|
|
43
|
+
* fold must split so that each prefix segment folds identically to how it
|
|
44
|
+
* folded when it was learned (§10.3 stable-prefix contract). Only the
|
|
45
|
+
* CALLER — who assembled the multi-turn context — knows where those
|
|
46
|
+
* boundaries are; the geometry never guesses them from the bytes. */
|
|
40
47
|
export function perceive(
|
|
41
48
|
ctx: MindContext,
|
|
42
49
|
input: Input,
|
|
43
50
|
leafAt?: (i: number) => number | null,
|
|
44
51
|
lookup?: (ids: number[]) => number | null,
|
|
52
|
+
boundaries?: readonly number[],
|
|
45
53
|
): Sema {
|
|
46
54
|
if (typeof input === "string" || input instanceof Uint8Array) {
|
|
47
55
|
const bytes = typeof input === "string"
|
|
@@ -57,11 +65,25 @@ export function perceive(
|
|
|
57
65
|
const key = latin1Key(bytes);
|
|
58
66
|
const hit = memo.get(key);
|
|
59
67
|
if (hit !== undefined) return hit;
|
|
60
|
-
const tree = bytesToTree(
|
|
68
|
+
const tree = bytesToTree(
|
|
69
|
+
ctx.space,
|
|
70
|
+
ctx.alphabet,
|
|
71
|
+
bytes,
|
|
72
|
+
undefined,
|
|
73
|
+
undefined,
|
|
74
|
+
boundaries,
|
|
75
|
+
);
|
|
61
76
|
memo.set(key, tree);
|
|
62
77
|
return tree;
|
|
63
78
|
}
|
|
64
|
-
return bytesToTree(
|
|
79
|
+
return bytesToTree(
|
|
80
|
+
ctx.space,
|
|
81
|
+
ctx.alphabet,
|
|
82
|
+
bytes,
|
|
83
|
+
undefined,
|
|
84
|
+
undefined,
|
|
85
|
+
boundaries,
|
|
86
|
+
);
|
|
65
87
|
}
|
|
66
88
|
return bytesToTree(ctx.space, ctx.alphabet, bytes, leafAt, lookup);
|
|
67
89
|
}
|
|
@@ -136,11 +158,25 @@ export function foldTree(
|
|
|
136
158
|
start: number,
|
|
137
159
|
visit?: (n: Sema, start: number, end: number, node: number | null) => void,
|
|
138
160
|
): { end: number; node: number | null } {
|
|
161
|
+
// Fast path: subtree already resolved (from a previous conversation turn
|
|
162
|
+
// or an earlier recognition pass). The pyramid reuses prefix subtrees as
|
|
163
|
+
// identical Sema objects, so this cache turns foldTree into O(suffix)
|
|
164
|
+
// instead of O(context) for multi-turn recognition.
|
|
165
|
+
const cached = ctx._resolvedSubtrees?.get(n);
|
|
166
|
+
if (cached !== undefined) {
|
|
167
|
+
const end = start + cached.len;
|
|
168
|
+
visit?.(n, start, end, cached.id);
|
|
169
|
+
return { end, node: cached.id };
|
|
170
|
+
}
|
|
171
|
+
|
|
139
172
|
if (n.kids === null) {
|
|
140
173
|
const b = n.leaf ?? new Uint8Array(0);
|
|
141
174
|
const end = start + b.length;
|
|
142
175
|
const node = ctx.store.findLeaf(b);
|
|
143
176
|
visit?.(n, start, end, node);
|
|
177
|
+
if (node !== null && ctx._resolvedSubtrees) {
|
|
178
|
+
ctx._resolvedSubtrees.set(n, { id: node, len: b.length });
|
|
179
|
+
}
|
|
144
180
|
return { end, node };
|
|
145
181
|
}
|
|
146
182
|
let pos = start;
|
|
@@ -154,6 +190,9 @@ export function foldTree(
|
|
|
154
190
|
}
|
|
155
191
|
const node = known ? ctx.store.findBranch(kids) : null;
|
|
156
192
|
visit?.(n, start, pos, node);
|
|
193
|
+
if (node !== null && ctx._resolvedSubtrees) {
|
|
194
|
+
ctx._resolvedSubtrees.set(n, { id: node, len: pos - start });
|
|
195
|
+
}
|
|
157
196
|
return { end: pos, node };
|
|
158
197
|
}
|
|
159
198
|
|
|
@@ -162,7 +201,70 @@ export function foldTree(
|
|
|
162
201
|
* unknown. */
|
|
163
202
|
export function resolve(ctx: MindContext, bytes: Uint8Array): number | null {
|
|
164
203
|
if (bytes.length === 0) return null;
|
|
165
|
-
|
|
204
|
+
const exact = foldTree(ctx, perceive(ctx, bytes), 0).node;
|
|
205
|
+
if (exact !== null) return exact;
|
|
206
|
+
return canonResolve(ctx, bytes);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Equivalence-class resolution: when the exact content-addressed lookup
|
|
210
|
+
* misses, find a stored node whose CANONICAL key equals the span's — the
|
|
211
|
+
* store's canon index proposes candidates by key hash, and each is verified
|
|
212
|
+
* by re-canonicalizing its bytes (hash-then-verify, like every content
|
|
213
|
+
* lookup). Among verified candidates, one that leads somewhere (has a
|
|
214
|
+
* continuation edge) is preferred; ties break to the lowest id — a corpus
|
|
215
|
+
* property, not a seed property. Null when the response carries no
|
|
216
|
+
* canonicalizer, the store has no canon index, or nothing verifies. */
|
|
217
|
+
export function canonResolve(
|
|
218
|
+
ctx: MindContext,
|
|
219
|
+
bytes: Uint8Array,
|
|
220
|
+
): number | null {
|
|
221
|
+
const canon = ctx.canon;
|
|
222
|
+
const store = ctx.store;
|
|
223
|
+
if (canon === null || !store.canonFind) return null;
|
|
224
|
+
if (bytes.length < 2) return null;
|
|
225
|
+
const memo = ctx.canonMemo;
|
|
226
|
+
const memoKey = memo ? latin1Key(bytes) : "";
|
|
227
|
+
if (memo) {
|
|
228
|
+
const hit = memo.get(memoKey);
|
|
229
|
+
if (hit !== undefined) return hit;
|
|
230
|
+
}
|
|
231
|
+
const set = (v: number | null): number | null => {
|
|
232
|
+
memo?.set(memoKey, v);
|
|
233
|
+
return v;
|
|
234
|
+
};
|
|
235
|
+
const key = canon(bytes);
|
|
236
|
+
if (key.length === 0) return set(null);
|
|
237
|
+
// A stored form that IS canonical is not in the index (buildCanonIndex
|
|
238
|
+
// skips identity rows) — the exact content-addressed lookup of the
|
|
239
|
+
// canonical bytes finds it directly.
|
|
240
|
+
if (key.length !== bytes.length || !bytesEqual(key, bytes)) {
|
|
241
|
+
const direct = foldTree(ctx, perceive(ctx, key), 0).node;
|
|
242
|
+
if (direct !== null) return set(direct);
|
|
243
|
+
}
|
|
244
|
+
const candidates = store.canonFind(canonHash(key));
|
|
245
|
+
if (candidates.length === 0) return set(null);
|
|
246
|
+
let best: number | null = null;
|
|
247
|
+
let bestLeads = false;
|
|
248
|
+
for (const id of candidates) {
|
|
249
|
+
const bytesOf = read(ctx, id);
|
|
250
|
+
const stored = canon(bytesOf);
|
|
251
|
+
if (stored.length !== key.length || !bytesEqual(stored, key)) continue;
|
|
252
|
+
// The index stores FLAT content twins; the id the exact path would have
|
|
253
|
+
// resolved for these bytes is their FOLD — the deposit-shaped node that
|
|
254
|
+
// carries the edges and halos. Re-folding the candidate's bytes lands
|
|
255
|
+
// on exactly the node the canonical-case query would have found.
|
|
256
|
+
const folded = foldTree(ctx, perceive(ctx, bytesOf), 0).node;
|
|
257
|
+
const use = folded ?? id;
|
|
258
|
+
const leads = store.hasNext(use) || store.haloMass(use) > 0;
|
|
259
|
+
if (
|
|
260
|
+
best === null || (leads && !bestLeads) ||
|
|
261
|
+
(leads === bestLeads && use < best)
|
|
262
|
+
) {
|
|
263
|
+
best = use;
|
|
264
|
+
bestLeads = leads;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return set(best);
|
|
166
268
|
}
|
|
167
269
|
|
|
168
270
|
/** Walk a perceived tree in POST-ORDER with byte offsets — children before
|
package/src/mind/recognition.ts
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
import { rItem } from "./trace.js";
|
|
8
8
|
|
|
9
9
|
import type { MindContext, Recognition, Segment } from "./types.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
canonResolve,
|
|
12
|
+
foldTree,
|
|
13
|
+
gistOf,
|
|
14
|
+
latin1Key,
|
|
15
|
+
perceive,
|
|
16
|
+
resolve,
|
|
17
|
+
} from "./primitives.js";
|
|
18
|
+
import { atomIsHub, corpusN, leadsSomewhere } from "./traverse.js";
|
|
12
19
|
import { chainReach, leafIdAt } from "./canonical.js";
|
|
13
20
|
import { isChunk, type Sema } from "../sema.js";
|
|
14
21
|
import type { Leaf, Site } from "./graph-search.js";
|
|
@@ -26,15 +33,15 @@ import type { Leaf, Site } from "./graph-search.js";
|
|
|
26
33
|
*
|
|
27
34
|
* Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
|
|
28
35
|
export function recognise(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// emits its rationale step.
|
|
36
|
+
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
37
|
+
// respondTurn() (where the map persists across calls). Skipped while
|
|
38
|
+
// tracing so every call still emits its rationale step.
|
|
33
39
|
if (ctx.recogniseMemo && !ctx.trace) {
|
|
34
|
-
const
|
|
40
|
+
const key = latin1Key(bytes);
|
|
41
|
+
const hit = ctx.recogniseMemo.get(key);
|
|
35
42
|
if (hit !== undefined) return hit;
|
|
36
43
|
const fresh = recogniseImpl(ctx, bytes);
|
|
37
|
-
ctx.recogniseMemo.set(
|
|
44
|
+
ctx.recogniseMemo.set(key, fresh);
|
|
38
45
|
return fresh;
|
|
39
46
|
}
|
|
40
47
|
return recogniseImpl(ctx, bytes);
|
|
@@ -64,7 +71,19 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
64
71
|
return id;
|
|
65
72
|
};
|
|
66
73
|
|
|
74
|
+
// Byte atoms (implicit negative-id single-byte leaves) are admitted as
|
|
75
|
+
// recognised sites only while atoms can still DISCRIMINATE at this corpus
|
|
76
|
+
// scale (see {@link atomIsHub}). On a small store a single-letter fact
|
|
77
|
+
// ("a" → "A") is genuine learnt content and its site is essential; on a
|
|
78
|
+
// large one every letter of every query would otherwise become a
|
|
79
|
+
// "recognised form" — the bridge then finds junction connectors between
|
|
80
|
+
// bare letters, cover follows edges hanging off them, and pure noise
|
|
81
|
+
// ("qq8f3kz9…") grounds to an arbitrary learnt sentence instead of
|
|
82
|
+
// silence. Atoms stay available as leaves (PASS-carried literals) and
|
|
83
|
+
// through exact tier-0 resolution regardless.
|
|
84
|
+
const atomsAreHubs = atomIsHub(ctx, corpusN(ctx));
|
|
67
85
|
const emit = (start: number, end: number, id: number) => {
|
|
86
|
+
if (id < 0 && atomsAreHubs) return;
|
|
68
87
|
if (leadsSomewhere(ctx, id)) {
|
|
69
88
|
sites.push({ start, end, payload: id });
|
|
70
89
|
}
|
|
@@ -78,6 +97,15 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
78
97
|
leaves.push({ start, end, bytes: n.leaf ?? new Uint8Array(0), node });
|
|
79
98
|
}
|
|
80
99
|
if (node !== null) emit(start, end, node);
|
|
100
|
+
// Canonical fallback: a subtree whose exact content-addressed lookup
|
|
101
|
+
// missed may still be a stored form under the response's equivalence
|
|
102
|
+
// (case, width, whitespace — whatever the injected canonicalizer says).
|
|
103
|
+
// O(subtree bytes) per miss, memoised per response; a no-op when no
|
|
104
|
+
// canonicalizer was injected or the store has no canon index.
|
|
105
|
+
else if (end - start >= 2) {
|
|
106
|
+
const cid = canonResolve(ctx, bytes.subarray(start, end));
|
|
107
|
+
if (cid !== null) emit(start, end, cid);
|
|
108
|
+
}
|
|
81
109
|
if (isChunk(n)) {
|
|
82
110
|
starts.add(start);
|
|
83
111
|
// Try every sub-span within this leaf-parent.
|
package/src/mind/traverse.ts
CHANGED
|
@@ -115,6 +115,30 @@ export function edgeAncestors(
|
|
|
115
115
|
const hit = memo?.get(id);
|
|
116
116
|
if (hit !== undefined) return hit;
|
|
117
117
|
|
|
118
|
+
// BYTE-ATOM COMMONALITY. A single-byte leaf (implicit negative id) has no
|
|
119
|
+
// structural parents BY CONSTRUCTION — atoms are never linked into the kid
|
|
120
|
+
// or contain tables — so this climb cannot observe its containment at all.
|
|
121
|
+
// The walk below would see only the atom's own direct edges and report
|
|
122
|
+
// contextsReached ≈ 1, turning the MOST common content in the store into
|
|
123
|
+
// the MOST discriminative voter (observed on a 325K-context store: every
|
|
124
|
+
// recognised single-letter site voted full ln N for the one fact whose
|
|
125
|
+
// continuation is that letter, and their pooled sum out-voted every
|
|
126
|
+
// genuine anchor). An unmeasurable containment must not default to
|
|
127
|
+
// "maximally rare": it is bounded below by the uniform expectation over
|
|
128
|
+
// the byte alphabet — N contexts, each at least one chunk of up to W of
|
|
129
|
+
// the 256 possible atoms, reach ≥ N·W/256 contexts per atom on average
|
|
130
|
+
// (see {@link atomReach}). When that floor itself exceeds the hub bound
|
|
131
|
+
// √N the atom is a hub at this corpus scale and the climb abstains
|
|
132
|
+
// (saturated) — the atom's own edges remain fully traversable (tier-0
|
|
133
|
+
// exact recall, chooseNext, project); only its say as a consensus voter
|
|
134
|
+
// is withdrawn. On a small store the floor stays ≤ √N and the atom
|
|
135
|
+
// climbs exactly as before, so single-letter facts keep working.
|
|
136
|
+
if (id < 0 && atomIsHub(ctx, contextCount)) {
|
|
137
|
+
const reach = { roots: [], contextsReached: 0, saturated: true };
|
|
138
|
+
memo?.set(id, reach);
|
|
139
|
+
return reach;
|
|
140
|
+
}
|
|
141
|
+
|
|
118
142
|
const bound = Math.ceil(Math.sqrt(contextCount));
|
|
119
143
|
const roots: number[] = [];
|
|
120
144
|
const seen = new Set<number>([id]);
|
|
@@ -257,6 +281,29 @@ export function prevOf(ctx: MindContext, id: number): number[] {
|
|
|
257
281
|
return ctx.store.prev(id);
|
|
258
282
|
}
|
|
259
283
|
|
|
284
|
+
/** The uniform-expectation floor on a byte atom's corpus commonality: N
|
|
285
|
+
* learnt contexts, each at least one perception chunk of up to W of the 256
|
|
286
|
+
* possible byte values, contain a given atom in ≥ N·W/256 contexts on
|
|
287
|
+
* average. An atom's TRUE containment is unmeasurable (atoms carry no
|
|
288
|
+
* kid/contain links by construction), so this floor is the honest stand-in:
|
|
289
|
+
* derived entirely from the corpus scale N, the perception window W, and
|
|
290
|
+
* the alphabet size — never tuned. */
|
|
291
|
+
export function atomReach(ctx: MindContext, contextCount: number): number {
|
|
292
|
+
return Math.max(
|
|
293
|
+
1,
|
|
294
|
+
Math.ceil((contextCount * ctx.space.maxGroup) / 256),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Whether a byte atom is a hub at this corpus scale — its commonality floor
|
|
299
|
+
* {@link atomReach} exceeds the hub bound √N. Below it (small stores) an
|
|
300
|
+
* atom votes and is recognised exactly as any stored form; above it the
|
|
301
|
+
* alphabet is scaffolding everywhere and abstains. */
|
|
302
|
+
export function atomIsHub(ctx: MindContext, contextCount: number): boolean {
|
|
303
|
+
return atomReach(ctx, contextCount) >
|
|
304
|
+
Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
305
|
+
}
|
|
306
|
+
|
|
260
307
|
/** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
|
|
261
308
|
* The admission predicate recognition filters sites with (HOW_IT_WORKS
|
|
262
309
|
* §15.3): a form that leads nowhere contributes nothing to any derivation.
|
package/src/mind/types.ts
CHANGED
|
@@ -144,28 +144,37 @@ export interface MindContext extends GraphSearchHost {
|
|
|
144
144
|
cfg: MindConfig;
|
|
145
145
|
search: GraphSearch;
|
|
146
146
|
trace: Rationale | null;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
/** Per-response memo of
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
*
|
|
147
|
+
/** The content canonicalizer for THIS response, or null — injected by the
|
|
148
|
+
* modality entry point (respondText passes the text canonicalizer; a
|
|
149
|
+
* binary respond passes none). Resolution uses it as a fallback: when
|
|
150
|
+
* the exact content-addressed lookup misses, the span's canonical key is
|
|
151
|
+
* probed against the store's canon index (see src/canon.ts). The core
|
|
152
|
+
* never inspects what the equivalence IS. */
|
|
153
|
+
canon: ((bytes: Uint8Array) => Uint8Array) | null;
|
|
154
|
+
/** Per-response memo of canonical-fallback resolutions, keyed by the
|
|
155
|
+
* span's latin1 content key. Null outside respond(). */
|
|
156
|
+
canonMemo: Map<string, number | null> | null;
|
|
157
|
+
/** Memo of the consensus climb — content-keyed (latin1) so results
|
|
158
|
+
* persist across conversation turns where the same byte spans recur.
|
|
159
|
+
* Null outside respond(); during respondTurn() the conversation's
|
|
160
|
+
* persistent map is swapped in. */
|
|
161
|
+
climbMemo: Map<string, Map<string, AttentionRead>> | null;
|
|
162
|
+
/** Memo of {@link recognise} — content-keyed (latin1) so recognised
|
|
163
|
+
* forms carry forward across conversation turns. Bypassed while a
|
|
164
|
+
* trace is attached. Null outside respond(). */
|
|
165
|
+
recogniseMemo: Map<string, Recognition> | null;
|
|
166
|
+
/** Memo of {@link perceive} — content-keyed (latin1). The general
|
|
167
|
+
* cache the result-level memos each partially compensate for. NOT
|
|
168
|
+
* bypassed under trace — perception emits no rationale steps.
|
|
169
|
+
* Null outside respond(). */
|
|
168
170
|
perceiveMemo: Map<string, Sema> | null;
|
|
171
|
+
/** Subtree-resolution cache: Sema node → its store id and byte length.
|
|
172
|
+
* Populated by {@link foldTree} during inference; checked before
|
|
173
|
+
* walking children. When a conversation's pyramid reuses prefix
|
|
174
|
+
* subtrees, this cache lets {@link recognise} skip them entirely —
|
|
175
|
+
* O(suffix) instead of O(context). Mind-lifetime (WeakMap keys are
|
|
176
|
+
* the Sema objects the pyramid keeps alive). */
|
|
177
|
+
_resolvedSubtrees: WeakMap<Sema, { id: number; len: number }> | null;
|
|
169
178
|
_edgeGuide: Vec | null;
|
|
170
179
|
_edgeChoice: Map<number, number>;
|
|
171
180
|
_prevSeen: Set<number> | null;
|
package/src/store-sqlite.ts
CHANGED
|
@@ -134,6 +134,18 @@ CREATE TABLE IF NOT EXISTS contain (
|
|
|
134
134
|
id INTEGER PRIMARY KEY,
|
|
135
135
|
parents BLOB NOT NULL
|
|
136
136
|
);
|
|
137
|
+
-- CANONICAL-FORM index (Store.canonAdd/canonFind): h is the 32-bit hash of a
|
|
138
|
+
-- node's CANONICAL content key (the modality's canonicalizer output — case-
|
|
139
|
+
-- folded, whitespace-collapsed text, etc.; the store never sees the key
|
|
140
|
+
-- itself, only its hash). Same hash-then-verify discipline as idx_node_h:
|
|
141
|
+
-- the caller re-canonicalizes each candidate's bytes before trusting it, so
|
|
142
|
+
-- a collision costs a read, never a wrong id. WITHOUT ROWID on (h, id):
|
|
143
|
+
-- the composite key IS the lookup index and gives free dedup.
|
|
144
|
+
CREATE TABLE IF NOT EXISTS canon (
|
|
145
|
+
h INTEGER NOT NULL,
|
|
146
|
+
id INTEGER NOT NULL,
|
|
147
|
+
PRIMARY KEY (h, id)
|
|
148
|
+
) WITHOUT ROWID;
|
|
137
149
|
CREATE TABLE IF NOT EXISTS snapshot (
|
|
138
150
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
139
151
|
data BLOB NOT NULL
|
|
@@ -241,6 +253,10 @@ export class SQliteStore extends AbstractStore implements Store {
|
|
|
241
253
|
private _selPrev: any = null;
|
|
242
254
|
private _setMeta: any = null;
|
|
243
255
|
private _getMeta: any = null;
|
|
256
|
+
private _insCanon: any = null;
|
|
257
|
+
private _selCanon: any = null;
|
|
258
|
+
private _cntCanon: any = null;
|
|
259
|
+
private _selContentFrom: any = null;
|
|
244
260
|
private _delMeta: any = null;
|
|
245
261
|
private _insSnapshot: any = null;
|
|
246
262
|
private _selSnapshot: any = null;
|
|
@@ -990,6 +1006,58 @@ export class SQliteStore extends AbstractStore implements Store {
|
|
|
990
1006
|
this._delMeta.run(key);
|
|
991
1007
|
}
|
|
992
1008
|
|
|
1009
|
+
// -- Canonical-form index (Store optional capability) --
|
|
1010
|
+
|
|
1011
|
+
canonAdd(h: number, id: number): void {
|
|
1012
|
+
if (!this._insCanon) {
|
|
1013
|
+
this._insCanon = this.sqlite!.prepare(
|
|
1014
|
+
"INSERT OR IGNORE INTO canon (h, id) VALUES (?, ?)",
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
// Join the deferred write transaction (committed by flush/commit), so a
|
|
1018
|
+
// bulk index build coalesces instead of paying autocommit per row.
|
|
1019
|
+
this._dbBeginTx();
|
|
1020
|
+
this._insCanon.run(h, id);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
canonFind(h: number): number[] {
|
|
1024
|
+
if (!this._selCanon) {
|
|
1025
|
+
this._selCanon = this.sqlite!.prepare(
|
|
1026
|
+
"SELECT id FROM canon WHERE h = ?",
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
return (this._selCanon.all(h) as Array<{ id: number }>).map((r) => r.id);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
canonCount(): number {
|
|
1033
|
+
if (!this._cntCanon) {
|
|
1034
|
+
this._cntCanon = this.sqlite!.prepare(
|
|
1035
|
+
"SELECT count(*) AS c FROM canon",
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
return (this._cntCanon.get() as { c: number }).c;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
eachContent(
|
|
1042
|
+
cb: (id: number, bytes: Uint8Array) => void,
|
|
1043
|
+
fromId = 0,
|
|
1044
|
+
): void {
|
|
1045
|
+
if (!this._selContentFrom) {
|
|
1046
|
+
// Content-bearing nodes are FLAT branches: leaf present, kids an
|
|
1047
|
+
// empty (zero-length) blob — the same population PART 2 of the
|
|
1048
|
+
// diagnostics calls "distinct content spans".
|
|
1049
|
+
this._selContentFrom = this.sqlite!.prepare(
|
|
1050
|
+
"SELECT id, leaf FROM node " +
|
|
1051
|
+
"WHERE id >= ? AND leaf IS NOT NULL " +
|
|
1052
|
+
"AND (kids IS NULL OR length(kids) = 0)",
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
for (const row of this._selContentFrom.iterate(fromId)) {
|
|
1056
|
+
const r = row as { id: number; leaf: Uint8Array };
|
|
1057
|
+
cb(r.id, new Uint8Array(r.leaf));
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
993
1061
|
// -- Snapshot --
|
|
994
1062
|
|
|
995
1063
|
protected _dbSaveSnapshot(bytes: Uint8Array): void {
|
|
@@ -1048,6 +1116,10 @@ export class SQliteStore extends AbstractStore implements Store {
|
|
|
1048
1116
|
return this.content ? this.content.physicalSize : 0;
|
|
1049
1117
|
}
|
|
1050
1118
|
|
|
1119
|
+
protected _vecContentClusterCount(): number {
|
|
1120
|
+
return this.content ? this.content.clusterCount : 0;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1051
1123
|
protected _vecContentCompact(): void {
|
|
1052
1124
|
this.content!.compact();
|
|
1053
1125
|
}
|
|
@@ -1099,6 +1171,10 @@ export class SQliteStore extends AbstractStore implements Store {
|
|
|
1099
1171
|
return this.halos ? this.halos.physicalSize : 0;
|
|
1100
1172
|
}
|
|
1101
1173
|
|
|
1174
|
+
protected _vecHaloClusterCount(): number {
|
|
1175
|
+
return this.halos ? this.halos.clusterCount : 0;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1102
1178
|
protected _vecHaloCompact(): void {
|
|
1103
1179
|
this.halos!.compact();
|
|
1104
1180
|
}
|
package/src/store.ts
CHANGED
|
@@ -417,6 +417,33 @@ export interface Store {
|
|
|
417
417
|
* the node has no halo row. */
|
|
418
418
|
haloMass(id: NodeId): number;
|
|
419
419
|
|
|
420
|
+
// ── canonical-form index (optional capability) ─────────────────────────
|
|
421
|
+
// A small hash index from CANONICAL content keys to node ids, enabling
|
|
422
|
+
// equivalence-class resolution ("WHAT" finds the stored "What") without
|
|
423
|
+
// the store knowing what the equivalence IS: the canonicalizer lives with
|
|
424
|
+
// the modality (see src/canon.ts) and is applied by the CALLER — the store
|
|
425
|
+
// only maps 32-bit key hashes to candidate ids, and the caller verifies
|
|
426
|
+
// canon(stored bytes) === key before trusting any candidate (the same
|
|
427
|
+
// hash-then-verify discipline as the node table's own `h` index).
|
|
428
|
+
// Backends that do not implement the capability leave all three absent;
|
|
429
|
+
// resolution then simply has no canonical fallback.
|
|
430
|
+
|
|
431
|
+
/** Record that node `id`'s canonical key hashes to `h`. Idempotent. */
|
|
432
|
+
canonAdd?(h: number, id: NodeId): void;
|
|
433
|
+
/** All candidate node ids whose canonical key hashes to `h` (collisions
|
|
434
|
+
* included — the caller verifies). */
|
|
435
|
+
canonFind?(h: number): NodeId[];
|
|
436
|
+
/** Number of (h, id) rows in the canon index — 0 means never built. */
|
|
437
|
+
canonCount?(): number;
|
|
438
|
+
/** Visit every content-bearing node (flat branch: `leaf` present, no
|
|
439
|
+
* kids) — the population a canonical index is built over. `fromId`
|
|
440
|
+
* restricts the scan to ids ≥ fromId, so an index refresh after further
|
|
441
|
+
* training only visits the new rows. */
|
|
442
|
+
eachContent?(
|
|
443
|
+
cb: (id: NodeId, bytes: Uint8Array) => void,
|
|
444
|
+
fromId?: NodeId,
|
|
445
|
+
): void;
|
|
446
|
+
|
|
420
447
|
// ── lifecycle ──────────────────────────────────────────────────────────
|
|
421
448
|
size(): Promise<number>;
|
|
422
449
|
saveSnapshot(bytes: Uint8Array): Promise<void>;
|
|
@@ -724,6 +751,7 @@ export abstract class AbstractStore implements Store {
|
|
|
724
751
|
protected abstract _vecContentSize(): number;
|
|
725
752
|
protected abstract _vecContentLastReads(): number;
|
|
726
753
|
protected abstract _vecContentPhysicalSize(): number;
|
|
754
|
+
protected abstract _vecContentClusterCount(): number;
|
|
727
755
|
protected abstract _vecContentCompact(): void;
|
|
728
756
|
/** Live content-index entries whose INTERNAL id is > `after`, as
|
|
729
757
|
* {ext, internal} pairs in internal-id order. Internal ids are monotone
|
|
@@ -759,8 +787,24 @@ export abstract class AbstractStore implements Store {
|
|
|
759
787
|
* the tombstone-ratio compaction trigger compares physical size against. */
|
|
760
788
|
protected abstract _vecHaloSize(): number;
|
|
761
789
|
protected abstract _vecHaloPhysicalSize(): number;
|
|
790
|
+
protected abstract _vecHaloClusterCount(): number;
|
|
762
791
|
protected abstract _vecHaloCompact(): void;
|
|
763
792
|
|
|
793
|
+
/** Derived query breadth for a partitioned index of C clusters: probe √C
|
|
794
|
+
* of them (the same √-of-the-population convention as the hub bound √N).
|
|
795
|
+
* The IVF maps ef → nprobe as ceil(ef/4), so ef = 4·⌈√C⌉ probes exactly
|
|
796
|
+
* ⌈√C⌉ clusters. A FIXED efSearch stops scaling the moment the
|
|
797
|
+
* collection outgrows it: at 4,270 clusters the default 64 probed 16
|
|
798
|
+
* clusters (0.4%), and an exact stored match of a query routinely sat in
|
|
799
|
+
* an unprobed cluster — recall silently degraded as the store grew. The
|
|
800
|
+
* configured efSearch remains the floor for small collections. */
|
|
801
|
+
protected efFor(clusterCount: number): number {
|
|
802
|
+
return Math.max(
|
|
803
|
+
this.efSearch,
|
|
804
|
+
4 * Math.ceil(Math.sqrt(Math.max(1, clusterCount))),
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
764
808
|
// ── Config ─────────────────────────────────────────────────────────────
|
|
765
809
|
|
|
766
810
|
protected _D: number;
|
|
@@ -1615,7 +1659,7 @@ export abstract class AbstractStore implements Store {
|
|
|
1615
1659
|
const results = this._vecContentQuery(
|
|
1616
1660
|
normalize(copy(v)),
|
|
1617
1661
|
k * this.overfetch,
|
|
1618
|
-
this.
|
|
1662
|
+
this.efFor(this._vecContentClusterCount()),
|
|
1619
1663
|
);
|
|
1620
1664
|
const out: Hit[] = [];
|
|
1621
1665
|
for (const r of results) {
|
|
@@ -1966,7 +2010,7 @@ export abstract class AbstractStore implements Store {
|
|
|
1966
2010
|
const results = this._vecHaloQuery(
|
|
1967
2011
|
normalize(copy(v)),
|
|
1968
2012
|
k * this.overfetch,
|
|
1969
|
-
this.
|
|
2013
|
+
this.efFor(this._vecHaloClusterCount()),
|
|
1970
2014
|
);
|
|
1971
2015
|
const out: Hit[] = [];
|
|
1972
2016
|
for (const r of results) {
|