@hviana/sema 0.1.6 → 0.1.7
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 +18 -5
- package/dist/src/geometry.d.ts +13 -2
- package/dist/src/geometry.js +89 -2
- package/dist/src/mind/attention.js +11 -7
- package/dist/src/mind/mechanisms/recall.js +26 -11
- package/dist/src/mind/mind.d.ts +75 -11
- package/dist/src/mind/mind.js +151 -18
- package/dist/src/mind/primitives.d.ts +13 -2
- package/dist/src/mind/primitives.js +27 -5
- package/dist/src/mind/recognition.js +21 -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 +23 -21
- package/dist/src/store-sqlite.d.ts +2 -0
- package/dist/src/store-sqlite.js +6 -0
- package/dist/src/store.d.ts +11 -0
- package/dist/src/store.js +13 -2
- package/example/train_base.ts +20 -5
- package/index.html +65 -0
- package/package.json +1 -1
- package/src/geometry.ts +93 -1
- package/src/mind/attention.ts +11 -6
- package/src/mind/mechanisms/recall.ts +29 -13
- package/src/mind/mind.ts +250 -19
- package/src/mind/primitives.ts +43 -6
- package/src/mind/recognition.ts +26 -8
- package/src/mind/traverse.ts +47 -0
- package/src/mind/types.ts +20 -21
- package/src/store-sqlite.ts +8 -0
- package/src/store.ts +19 -2
- package/test/13-conversation.test.mjs +190 -20
|
@@ -9,7 +9,7 @@ import { ALL } from "./types.js";
|
|
|
9
9
|
* collision-free encoding. Spans on the perception path are query-scale
|
|
10
10
|
* (windows, regions, candidate spans), so key construction is far cheaper
|
|
11
11
|
* than the river fold it deduplicates. */
|
|
12
|
-
function latin1Key(bytes) {
|
|
12
|
+
export function latin1Key(bytes) {
|
|
13
13
|
// Batched String.fromCharCode — avoids the O(n²) cost of repeated += on
|
|
14
14
|
// potentially-large query spans, and stays well under the ~65536 arg limit.
|
|
15
15
|
const n = bytes.length;
|
|
@@ -20,8 +20,14 @@ function latin1Key(bytes) {
|
|
|
20
20
|
return s;
|
|
21
21
|
}
|
|
22
22
|
/** Perceive input into a content-defined tree (the river fold).
|
|
23
|
-
* Deterministic — identical bytes always produce an identical tree.
|
|
24
|
-
|
|
23
|
+
* Deterministic — identical bytes always produce an identical tree.
|
|
24
|
+
*
|
|
25
|
+
* `boundaries` is an optional sorted list of proper byte offsets where the
|
|
26
|
+
* fold must split so that each prefix segment folds identically to how it
|
|
27
|
+
* folded when it was learned (§10.3 stable-prefix contract). Only the
|
|
28
|
+
* CALLER — who assembled the multi-turn context — knows where those
|
|
29
|
+
* boundaries are; the geometry never guesses them from the bytes. */
|
|
30
|
+
export function perceive(ctx, input, leafAt, lookup, boundaries) {
|
|
25
31
|
if (typeof input === "string" || input instanceof Uint8Array) {
|
|
26
32
|
const bytes = typeof input === "string"
|
|
27
33
|
? new TextEncoder().encode(input)
|
|
@@ -37,11 +43,11 @@ export function perceive(ctx, input, leafAt, lookup) {
|
|
|
37
43
|
const hit = memo.get(key);
|
|
38
44
|
if (hit !== undefined)
|
|
39
45
|
return hit;
|
|
40
|
-
const tree = bytesToTree(ctx.space, ctx.alphabet, bytes);
|
|
46
|
+
const tree = bytesToTree(ctx.space, ctx.alphabet, bytes, undefined, undefined, boundaries);
|
|
41
47
|
memo.set(key, tree);
|
|
42
48
|
return tree;
|
|
43
49
|
}
|
|
44
|
-
return bytesToTree(ctx.space, ctx.alphabet, bytes);
|
|
50
|
+
return bytesToTree(ctx.space, ctx.alphabet, bytes, undefined, undefined, boundaries);
|
|
45
51
|
}
|
|
46
52
|
return bytesToTree(ctx.space, ctx.alphabet, bytes, leafAt, lookup);
|
|
47
53
|
}
|
|
@@ -105,11 +111,24 @@ export function gistOf(ctx, bytes) {
|
|
|
105
111
|
* node with its byte span and resolved id. Returns the node's byte end and
|
|
106
112
|
* resolved id. */
|
|
107
113
|
export function foldTree(ctx, n, start, visit) {
|
|
114
|
+
// Fast path: subtree already resolved (from a previous conversation turn
|
|
115
|
+
// or an earlier recognition pass). The pyramid reuses prefix subtrees as
|
|
116
|
+
// identical Sema objects, so this cache turns foldTree into O(suffix)
|
|
117
|
+
// instead of O(context) for multi-turn recognition.
|
|
118
|
+
const cached = ctx._resolvedSubtrees?.get(n);
|
|
119
|
+
if (cached !== undefined) {
|
|
120
|
+
const end = start + cached.len;
|
|
121
|
+
visit?.(n, start, end, cached.id);
|
|
122
|
+
return { end, node: cached.id };
|
|
123
|
+
}
|
|
108
124
|
if (n.kids === null) {
|
|
109
125
|
const b = n.leaf ?? new Uint8Array(0);
|
|
110
126
|
const end = start + b.length;
|
|
111
127
|
const node = ctx.store.findLeaf(b);
|
|
112
128
|
visit?.(n, start, end, node);
|
|
129
|
+
if (node !== null && ctx._resolvedSubtrees) {
|
|
130
|
+
ctx._resolvedSubtrees.set(n, { id: node, len: b.length });
|
|
131
|
+
}
|
|
113
132
|
return { end, node };
|
|
114
133
|
}
|
|
115
134
|
let pos = start;
|
|
@@ -125,6 +144,9 @@ export function foldTree(ctx, n, start, visit) {
|
|
|
125
144
|
}
|
|
126
145
|
const node = known ? ctx.store.findBranch(kids) : null;
|
|
127
146
|
visit?.(n, start, pos, node);
|
|
147
|
+
if (node !== null && ctx._resolvedSubtrees) {
|
|
148
|
+
ctx._resolvedSubtrees.set(n, { id: node, len: pos - start });
|
|
149
|
+
}
|
|
128
150
|
return { end: pos, node };
|
|
129
151
|
}
|
|
130
152
|
/** The canonical node id of a byte span: perceive it in isolation — the way
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// that leads somewhere (has a continuation edge or a halo).
|
|
6
6
|
// segment — leaf-parent segmentation using the geometry's own groupings.
|
|
7
7
|
import { rItem } from "./trace.js";
|
|
8
|
-
import { foldTree, gistOf, perceive, resolve } from "./primitives.js";
|
|
9
|
-
import { leadsSomewhere } from "./traverse.js";
|
|
8
|
+
import { foldTree, gistOf, latin1Key, perceive, resolve, } from "./primitives.js";
|
|
9
|
+
import { atomIsHub, corpusN, leadsSomewhere } from "./traverse.js";
|
|
10
10
|
import { chainReach, leafIdAt } from "./canonical.js";
|
|
11
11
|
import { isChunk } from "../sema.js";
|
|
12
12
|
/** Decompose a byte stream into every stored form that leads somewhere
|
|
@@ -22,16 +22,16 @@ import { isChunk } from "../sema.js";
|
|
|
22
22
|
*
|
|
23
23
|
* Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
|
|
24
24
|
export function recognise(ctx, bytes) {
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// emits its rationale step.
|
|
25
|
+
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
26
|
+
// respondTurn() (where the map persists across calls). Skipped while
|
|
27
|
+
// tracing so every call still emits its rationale step.
|
|
29
28
|
if (ctx.recogniseMemo && !ctx.trace) {
|
|
30
|
-
const
|
|
29
|
+
const key = latin1Key(bytes);
|
|
30
|
+
const hit = ctx.recogniseMemo.get(key);
|
|
31
31
|
if (hit !== undefined)
|
|
32
32
|
return hit;
|
|
33
33
|
const fresh = recogniseImpl(ctx, bytes);
|
|
34
|
-
ctx.recogniseMemo.set(
|
|
34
|
+
ctx.recogniseMemo.set(key, fresh);
|
|
35
35
|
return fresh;
|
|
36
36
|
}
|
|
37
37
|
return recogniseImpl(ctx, bytes);
|
|
@@ -59,7 +59,20 @@ function recogniseImpl(ctx, bytes) {
|
|
|
59
59
|
}
|
|
60
60
|
return id;
|
|
61
61
|
};
|
|
62
|
+
// Byte atoms (implicit negative-id single-byte leaves) are admitted as
|
|
63
|
+
// recognised sites only while atoms can still DISCRIMINATE at this corpus
|
|
64
|
+
// scale (see {@link atomIsHub}). On a small store a single-letter fact
|
|
65
|
+
// ("a" → "A") is genuine learnt content and its site is essential; on a
|
|
66
|
+
// large one every letter of every query would otherwise become a
|
|
67
|
+
// "recognised form" — the bridge then finds junction connectors between
|
|
68
|
+
// bare letters, cover follows edges hanging off them, and pure noise
|
|
69
|
+
// ("qq8f3kz9…") grounds to an arbitrary learnt sentence instead of
|
|
70
|
+
// silence. Atoms stay available as leaves (PASS-carried literals) and
|
|
71
|
+
// through exact tier-0 resolution regardless.
|
|
72
|
+
const atomsAreHubs = atomIsHub(ctx, corpusN(ctx));
|
|
62
73
|
const emit = (start, end, id) => {
|
|
74
|
+
if (id < 0 && atomsAreHubs)
|
|
75
|
+
return;
|
|
63
76
|
if (leadsSomewhere(ctx, id)) {
|
|
64
77
|
sites.push({ start, end, payload: id });
|
|
65
78
|
}
|
|
@@ -16,6 +16,19 @@ export declare function edgeAncestors(ctx: MindContext, id: number, contextCount
|
|
|
16
16
|
export declare function nextOf(ctx: MindContext, id: number): number[];
|
|
17
17
|
/** Convenience: reverse edges of a node. */
|
|
18
18
|
export declare function prevOf(ctx: MindContext, id: number): number[];
|
|
19
|
+
/** The uniform-expectation floor on a byte atom's corpus commonality: N
|
|
20
|
+
* learnt contexts, each at least one perception chunk of up to W of the 256
|
|
21
|
+
* possible byte values, contain a given atom in ≥ N·W/256 contexts on
|
|
22
|
+
* average. An atom's TRUE containment is unmeasurable (atoms carry no
|
|
23
|
+
* kid/contain links by construction), so this floor is the honest stand-in:
|
|
24
|
+
* derived entirely from the corpus scale N, the perception window W, and
|
|
25
|
+
* the alphabet size — never tuned. */
|
|
26
|
+
export declare function atomReach(ctx: MindContext, contextCount: number): number;
|
|
27
|
+
/** Whether a byte atom is a hub at this corpus scale — its commonality floor
|
|
28
|
+
* {@link atomReach} exceeds the hub bound √N. Below it (small stores) an
|
|
29
|
+
* atom votes and is recognised exactly as any stored form; above it the
|
|
30
|
+
* alphabet is scaffolding everywhere and abstains. */
|
|
31
|
+
export declare function atomIsHub(ctx: MindContext, contextCount: number): boolean;
|
|
19
32
|
/** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
|
|
20
33
|
* The admission predicate recognition filters sites with (HOW_IT_WORKS
|
|
21
34
|
* §15.3): a form that leads nowhere contributes nothing to any derivation.
|
|
@@ -70,6 +70,29 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
70
70
|
const hit = memo?.get(id);
|
|
71
71
|
if (hit !== undefined)
|
|
72
72
|
return hit;
|
|
73
|
+
// BYTE-ATOM COMMONALITY. A single-byte leaf (implicit negative id) has no
|
|
74
|
+
// structural parents BY CONSTRUCTION — atoms are never linked into the kid
|
|
75
|
+
// or contain tables — so this climb cannot observe its containment at all.
|
|
76
|
+
// The walk below would see only the atom's own direct edges and report
|
|
77
|
+
// contextsReached ≈ 1, turning the MOST common content in the store into
|
|
78
|
+
// the MOST discriminative voter (observed on a 325K-context store: every
|
|
79
|
+
// recognised single-letter site voted full ln N for the one fact whose
|
|
80
|
+
// continuation is that letter, and their pooled sum out-voted every
|
|
81
|
+
// genuine anchor). An unmeasurable containment must not default to
|
|
82
|
+
// "maximally rare": it is bounded below by the uniform expectation over
|
|
83
|
+
// the byte alphabet — N contexts, each at least one chunk of up to W of
|
|
84
|
+
// the 256 possible atoms, reach ≥ N·W/256 contexts per atom on average
|
|
85
|
+
// (see {@link atomReach}). When that floor itself exceeds the hub bound
|
|
86
|
+
// √N the atom is a hub at this corpus scale and the climb abstains
|
|
87
|
+
// (saturated) — the atom's own edges remain fully traversable (tier-0
|
|
88
|
+
// exact recall, chooseNext, project); only its say as a consensus voter
|
|
89
|
+
// is withdrawn. On a small store the floor stays ≤ √N and the atom
|
|
90
|
+
// climbs exactly as before, so single-letter facts keep working.
|
|
91
|
+
if (id < 0 && atomIsHub(ctx, contextCount)) {
|
|
92
|
+
const reach = { roots: [], contextsReached: 0, saturated: true };
|
|
93
|
+
memo?.set(id, reach);
|
|
94
|
+
return reach;
|
|
95
|
+
}
|
|
73
96
|
const bound = Math.ceil(Math.sqrt(contextCount));
|
|
74
97
|
const roots = [];
|
|
75
98
|
const seen = new Set([id]);
|
|
@@ -214,6 +237,24 @@ export function nextOf(ctx, id) {
|
|
|
214
237
|
export function prevOf(ctx, id) {
|
|
215
238
|
return ctx.store.prev(id);
|
|
216
239
|
}
|
|
240
|
+
/** The uniform-expectation floor on a byte atom's corpus commonality: N
|
|
241
|
+
* learnt contexts, each at least one perception chunk of up to W of the 256
|
|
242
|
+
* possible byte values, contain a given atom in ≥ N·W/256 contexts on
|
|
243
|
+
* average. An atom's TRUE containment is unmeasurable (atoms carry no
|
|
244
|
+
* kid/contain links by construction), so this floor is the honest stand-in:
|
|
245
|
+
* derived entirely from the corpus scale N, the perception window W, and
|
|
246
|
+
* the alphabet size — never tuned. */
|
|
247
|
+
export function atomReach(ctx, contextCount) {
|
|
248
|
+
return Math.max(1, Math.ceil((contextCount * ctx.space.maxGroup) / 256));
|
|
249
|
+
}
|
|
250
|
+
/** Whether a byte atom is a hub at this corpus scale — its commonality floor
|
|
251
|
+
* {@link atomReach} exceeds the hub bound √N. Below it (small stores) an
|
|
252
|
+
* atom votes and is recognised exactly as any stored form; above it the
|
|
253
|
+
* alphabet is scaffolding everywhere and abstains. */
|
|
254
|
+
export function atomIsHub(ctx, contextCount) {
|
|
255
|
+
return atomReach(ctx, contextCount) >
|
|
256
|
+
Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
257
|
+
}
|
|
217
258
|
/** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
|
|
218
259
|
* The admission predicate recognition filters sites with (HOW_IT_WORKS
|
|
219
260
|
* §15.3): a form that leads nowhere contributes nothing to any derivation.
|
package/dist/src/mind/types.d.ts
CHANGED
|
@@ -105,28 +105,30 @@ export interface MindContext extends GraphSearchHost {
|
|
|
105
105
|
cfg: MindConfig;
|
|
106
106
|
search: GraphSearch;
|
|
107
107
|
trace: Rationale | null;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* (
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* a pure function of bytes; only inference-shaped calls (plain Uint8Array,
|
|
122
|
-
* no leafAt/lookup capabilities) are memoised, so the deposit path never
|
|
123
|
-
* sees it. Keyed by CONTENT (latin1 of the bytes), not object identity —
|
|
124
|
-
* mechanisms materialise the same span in fresh subarrays constantly
|
|
125
|
-
* (measured on a trained store: 46% of one response's perceptions were
|
|
126
|
-
* byte-identical repeats an identity key missed). NOT bypassed under
|
|
127
|
-
* trace — perception emits no rationale steps, so there is nothing a memo
|
|
128
|
-
* hit could swallow. Null outside respond(). */
|
|
108
|
+
/** Memo of the consensus climb — content-keyed (latin1) so results
|
|
109
|
+
* persist across conversation turns where the same byte spans recur.
|
|
110
|
+
* Null outside respond(); during respondTurn() the conversation's
|
|
111
|
+
* persistent map is swapped in. */
|
|
112
|
+
climbMemo: Map<string, Map<string, AttentionRead>> | null;
|
|
113
|
+
/** Memo of {@link recognise} — content-keyed (latin1) so recognised
|
|
114
|
+
* forms carry forward across conversation turns. Bypassed while a
|
|
115
|
+
* trace is attached. Null outside respond(). */
|
|
116
|
+
recogniseMemo: Map<string, Recognition> | null;
|
|
117
|
+
/** Memo of {@link perceive} — content-keyed (latin1). The general
|
|
118
|
+
* cache the result-level memos each partially compensate for. NOT
|
|
119
|
+
* bypassed under trace — perception emits no rationale steps.
|
|
120
|
+
* Null outside respond(). */
|
|
129
121
|
perceiveMemo: Map<string, Sema> | null;
|
|
122
|
+
/** Subtree-resolution cache: Sema node → its store id and byte length.
|
|
123
|
+
* Populated by {@link foldTree} during inference; checked before
|
|
124
|
+
* walking children. When a conversation's pyramid reuses prefix
|
|
125
|
+
* subtrees, this cache lets {@link recognise} skip them entirely —
|
|
126
|
+
* O(suffix) instead of O(context). Mind-lifetime (WeakMap keys are
|
|
127
|
+
* the Sema objects the pyramid keeps alive). */
|
|
128
|
+
_resolvedSubtrees: WeakMap<Sema, {
|
|
129
|
+
id: number;
|
|
130
|
+
len: number;
|
|
131
|
+
}> | null;
|
|
130
132
|
_edgeGuide: Vec | null;
|
|
131
133
|
_edgeChoice: Map<number, number>;
|
|
132
134
|
_prevSeen: Set<number> | null;
|
|
@@ -144,6 +144,7 @@ export declare class SQliteStore extends AbstractStore implements Store {
|
|
|
144
144
|
protected _vecContentSize(): number;
|
|
145
145
|
protected _vecContentLastReads(): number;
|
|
146
146
|
protected _vecContentPhysicalSize(): number;
|
|
147
|
+
protected _vecContentClusterCount(): number;
|
|
147
148
|
protected _vecContentCompact(): void;
|
|
148
149
|
protected _vecContentDeleteMany(ids: NodeId[]): void;
|
|
149
150
|
protected _vecContentEntriesSince(after: number): IterableIterator<{
|
|
@@ -164,6 +165,7 @@ export declare class SQliteStore extends AbstractStore implements Store {
|
|
|
164
165
|
}>;
|
|
165
166
|
protected _vecHaloSize(): number;
|
|
166
167
|
protected _vecHaloPhysicalSize(): number;
|
|
168
|
+
protected _vecHaloClusterCount(): number;
|
|
167
169
|
protected _vecHaloCompact(): void;
|
|
168
170
|
/** Pre-fill both vector indices' RAM caches with sequential scans (up to
|
|
169
171
|
* their budget caps) — seconds of streaming instead of the minutes of
|
package/dist/src/store-sqlite.js
CHANGED
|
@@ -835,6 +835,9 @@ export class SQliteStore extends AbstractStore {
|
|
|
835
835
|
_vecContentPhysicalSize() {
|
|
836
836
|
return this.content ? this.content.physicalSize : 0;
|
|
837
837
|
}
|
|
838
|
+
_vecContentClusterCount() {
|
|
839
|
+
return this.content ? this.content.clusterCount : 0;
|
|
840
|
+
}
|
|
838
841
|
_vecContentCompact() {
|
|
839
842
|
this.content.compact();
|
|
840
843
|
}
|
|
@@ -869,6 +872,9 @@ export class SQliteStore extends AbstractStore {
|
|
|
869
872
|
_vecHaloPhysicalSize() {
|
|
870
873
|
return this.halos ? this.halos.physicalSize : 0;
|
|
871
874
|
}
|
|
875
|
+
_vecHaloClusterCount() {
|
|
876
|
+
return this.halos ? this.halos.clusterCount : 0;
|
|
877
|
+
}
|
|
872
878
|
_vecHaloCompact() {
|
|
873
879
|
this.halos.compact();
|
|
874
880
|
}
|
package/dist/src/store.d.ts
CHANGED
|
@@ -336,6 +336,7 @@ export declare abstract class AbstractStore implements Store {
|
|
|
336
336
|
protected abstract _vecContentSize(): number;
|
|
337
337
|
protected abstract _vecContentLastReads(): number;
|
|
338
338
|
protected abstract _vecContentPhysicalSize(): number;
|
|
339
|
+
protected abstract _vecContentClusterCount(): number;
|
|
339
340
|
protected abstract _vecContentCompact(): void;
|
|
340
341
|
/** Live content-index entries whose INTERNAL id is > `after`, as
|
|
341
342
|
* {ext, internal} pairs in internal-id order. Internal ids are monotone
|
|
@@ -370,7 +371,17 @@ export declare abstract class AbstractStore implements Store {
|
|
|
370
371
|
* the tombstone-ratio compaction trigger compares physical size against. */
|
|
371
372
|
protected abstract _vecHaloSize(): number;
|
|
372
373
|
protected abstract _vecHaloPhysicalSize(): number;
|
|
374
|
+
protected abstract _vecHaloClusterCount(): number;
|
|
373
375
|
protected abstract _vecHaloCompact(): void;
|
|
376
|
+
/** Derived query breadth for a partitioned index of C clusters: probe √C
|
|
377
|
+
* of them (the same √-of-the-population convention as the hub bound √N).
|
|
378
|
+
* The IVF maps ef → nprobe as ceil(ef/4), so ef = 4·⌈√C⌉ probes exactly
|
|
379
|
+
* ⌈√C⌉ clusters. A FIXED efSearch stops scaling the moment the
|
|
380
|
+
* collection outgrows it: at 4,270 clusters the default 64 probed 16
|
|
381
|
+
* clusters (0.4%), and an exact stored match of a query routinely sat in
|
|
382
|
+
* an unprobed cluster — recall silently degraded as the store grew. The
|
|
383
|
+
* configured efSearch remains the floor for small collections. */
|
|
384
|
+
protected efFor(clusterCount: number): number;
|
|
374
385
|
protected _D: number;
|
|
375
386
|
protected _maxGroup: number;
|
|
376
387
|
protected readonly minHaloMass: number;
|
package/dist/src/store.js
CHANGED
|
@@ -359,6 +359,17 @@ function haloDecode(blob, D) {
|
|
|
359
359
|
* protected abstract methods that talk to the actual storage backend.
|
|
360
360
|
*/
|
|
361
361
|
export class AbstractStore {
|
|
362
|
+
/** Derived query breadth for a partitioned index of C clusters: probe √C
|
|
363
|
+
* of them (the same √-of-the-population convention as the hub bound √N).
|
|
364
|
+
* The IVF maps ef → nprobe as ceil(ef/4), so ef = 4·⌈√C⌉ probes exactly
|
|
365
|
+
* ⌈√C⌉ clusters. A FIXED efSearch stops scaling the moment the
|
|
366
|
+
* collection outgrows it: at 4,270 clusters the default 64 probed 16
|
|
367
|
+
* clusters (0.4%), and an exact stored match of a query routinely sat in
|
|
368
|
+
* an unprobed cluster — recall silently degraded as the store grew. The
|
|
369
|
+
* configured efSearch remains the floor for small collections. */
|
|
370
|
+
efFor(clusterCount) {
|
|
371
|
+
return Math.max(this.efSearch, 4 * Math.ceil(Math.sqrt(Math.max(1, clusterCount))));
|
|
372
|
+
}
|
|
362
373
|
// ── Config ─────────────────────────────────────────────────────────────
|
|
363
374
|
_D;
|
|
364
375
|
_maxGroup;
|
|
@@ -1144,7 +1155,7 @@ export class AbstractStore {
|
|
|
1144
1155
|
if (hit !== undefined)
|
|
1145
1156
|
return hit;
|
|
1146
1157
|
}
|
|
1147
|
-
const results = this._vecContentQuery(normalize(copy(v)), k * this.overfetch, this.
|
|
1158
|
+
const results = this._vecContentQuery(normalize(copy(v)), k * this.overfetch, this.efFor(this._vecContentClusterCount()));
|
|
1148
1159
|
const out = [];
|
|
1149
1160
|
for (const r of results) {
|
|
1150
1161
|
const id = r.id;
|
|
@@ -1459,7 +1470,7 @@ export class AbstractStore {
|
|
|
1459
1470
|
if (hit !== undefined)
|
|
1460
1471
|
return hit;
|
|
1461
1472
|
}
|
|
1462
|
-
const results = this._vecHaloQuery(normalize(copy(v)), k * this.overfetch, this.
|
|
1473
|
+
const results = this._vecHaloQuery(normalize(copy(v)), k * this.overfetch, this.efFor(this._vecHaloClusterCount()));
|
|
1463
1474
|
const out = [];
|
|
1464
1475
|
for (const r of results) {
|
|
1465
1476
|
const id = r.id;
|
package/example/train_base.ts
CHANGED
|
@@ -1825,9 +1825,22 @@ async function main(): Promise<void> {
|
|
|
1825
1825
|
|
|
1826
1826
|
/** Run index maintenance: compact (remove garbage) then repair (fill gaps).
|
|
1827
1827
|
* Both are idempotent — running twice produces the same result as once.
|
|
1828
|
-
* Compaction frees index space first; repair then adds back
|
|
1829
|
-
* whose
|
|
1830
|
-
*
|
|
1828
|
+
* Compaction frees index space first; repair then adds back every
|
|
1829
|
+
* edge/halo-bearing node whose gist was evicted from the pending cache
|
|
1830
|
+
* before it reached the content index, completing the coverage that
|
|
1831
|
+
* incremental promotion alone cannot guarantee.
|
|
1832
|
+
*
|
|
1833
|
+
* repair runs with minParents = 0, NOT the library default of 2. The
|
|
1834
|
+
* default repairs only structural BRIDGES (≥2 parents), but this
|
|
1835
|
+
* trainer's fact deposits also leave answer-side DEPOSIT ROOTS with 0
|
|
1836
|
+
* structural parents ("The capital of France is Paris." as the dst of a
|
|
1837
|
+
* Q→A edge is a root of its own tree, contained in nothing). Those are
|
|
1838
|
+
* resonance targets recall depends on — a trained store shipped without
|
|
1839
|
+
* them cannot ground statement-shaped queries against its own answers
|
|
1840
|
+
* (observed: 33 such roots missing after a full curriculum, including
|
|
1841
|
+
* high-traffic conversation replies). minParents = 0 admits every
|
|
1842
|
+
* edge/halo bearer; the candidate set is still corpus-of-experiences-
|
|
1843
|
+
* sized, so the pass stays cheap.
|
|
1831
1844
|
*
|
|
1832
1845
|
* Logs the number of entries removed/added so a run that silently degrades
|
|
1833
1846
|
* (growing compaction count, or repair never recovering anything) is
|
|
@@ -1849,10 +1862,12 @@ async function main(): Promise<void> {
|
|
|
1849
1862
|
);
|
|
1850
1863
|
}
|
|
1851
1864
|
try {
|
|
1852
|
-
const added = await mind.repairContentIndex();
|
|
1865
|
+
const added = await mind.repairContentIndex(0);
|
|
1853
1866
|
if (added > 0) {
|
|
1854
1867
|
progress.log(
|
|
1855
|
-
` ${GRN}index repair: added ${
|
|
1868
|
+
` ${GRN}index repair: added ${
|
|
1869
|
+
int(added)
|
|
1870
|
+
} missing resonance targets${R}`,
|
|
1856
1871
|
);
|
|
1857
1872
|
}
|
|
1858
1873
|
} catch (err) {
|
package/index.html
CHANGED
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
z-index: 0;
|
|
53
53
|
overflow: hidden;
|
|
54
54
|
background: var(--bg);
|
|
55
|
+
/* own stacking context + GPU layer: keeps the blend-mode orbs from
|
|
56
|
+
forcing full-page re-rasterization every frame on mobile */
|
|
57
|
+
isolation: isolate;
|
|
58
|
+
transform: translateZ(0);
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/* — colour weather: two huge drifting orbs + aurora sweep — */
|
|
@@ -60,6 +64,7 @@
|
|
|
60
64
|
border-radius: 50%;
|
|
61
65
|
filter: blur(70px);
|
|
62
66
|
mix-blend-mode: screen;
|
|
67
|
+
will-change: transform;
|
|
63
68
|
}
|
|
64
69
|
.orb.a {
|
|
65
70
|
width: 75vmax;
|
|
@@ -130,6 +135,7 @@
|
|
|
130
135
|
transparent 360deg
|
|
131
136
|
);
|
|
132
137
|
animation: spin 70s linear infinite;
|
|
138
|
+
will-change: transform;
|
|
133
139
|
}
|
|
134
140
|
@keyframes spin {
|
|
135
141
|
to {
|
|
@@ -158,6 +164,7 @@
|
|
|
158
164
|
);
|
|
159
165
|
animation: drift 50s linear infinite alternate;
|
|
160
166
|
opacity: 0.8;
|
|
167
|
+
will-change: transform;
|
|
161
168
|
}
|
|
162
169
|
@keyframes drift {
|
|
163
170
|
to {
|
|
@@ -180,10 +187,12 @@
|
|
|
180
187
|
.layerA {
|
|
181
188
|
animation: sway 30s ease-in-out infinite alternate;
|
|
182
189
|
transform-origin: 50% 50%;
|
|
190
|
+
will-change: transform;
|
|
183
191
|
}
|
|
184
192
|
.layerB {
|
|
185
193
|
animation: sway 44s ease-in-out infinite alternate-reverse;
|
|
186
194
|
transform-origin: 50% 50%;
|
|
195
|
+
will-change: transform;
|
|
187
196
|
}
|
|
188
197
|
@keyframes sway {
|
|
189
198
|
from {
|
|
@@ -341,6 +350,7 @@
|
|
|
341
350
|
opacity: 0.5;
|
|
342
351
|
animation: spin 40s linear infinite;
|
|
343
352
|
filter: drop-shadow(0 0 30px rgba(57, 230, 255, 0.3));
|
|
353
|
+
will-change: transform;
|
|
344
354
|
}
|
|
345
355
|
.hexring::before {
|
|
346
356
|
content: "";
|
|
@@ -373,6 +383,7 @@
|
|
|
373
383
|
transparent 70%
|
|
374
384
|
);
|
|
375
385
|
animation: breath 7s ease-in-out infinite;
|
|
386
|
+
will-change: transform, opacity;
|
|
376
387
|
}
|
|
377
388
|
@keyframes breath {
|
|
378
389
|
0%, 100% {
|
|
@@ -423,6 +434,11 @@
|
|
|
423
434
|
filter: drop-shadow(0 0 22px rgba(57, 230, 255, 0.35)) drop-shadow(
|
|
424
435
|
0 8px 60px rgba(168, 120, 255, 0.25)
|
|
425
436
|
);
|
|
437
|
+
/* gradient-clipped text flickers on iOS while its background animates
|
|
438
|
+
unless the element sits on its own composited layer */
|
|
439
|
+
-webkit-backface-visibility: hidden;
|
|
440
|
+
backface-visibility: hidden;
|
|
441
|
+
transform: translateZ(0);
|
|
426
442
|
}
|
|
427
443
|
@keyframes sheen {
|
|
428
444
|
0%, 100% {
|
|
@@ -450,6 +466,7 @@
|
|
|
450
466
|
color: transparent;
|
|
451
467
|
-webkit-text-stroke: 1px rgba(57, 230, 255, 0.22);
|
|
452
468
|
animation: echo 8s ease-in-out infinite;
|
|
469
|
+
will-change: transform, opacity;
|
|
453
470
|
}
|
|
454
471
|
@keyframes echo {
|
|
455
472
|
0%, 100% {
|
|
@@ -594,6 +611,7 @@
|
|
|
594
611
|
white-space: nowrap;
|
|
595
612
|
width: max-content;
|
|
596
613
|
animation: scroll 34s linear infinite;
|
|
614
|
+
will-change: transform;
|
|
597
615
|
}
|
|
598
616
|
.ticker span {
|
|
599
617
|
padding: 0.72em 0;
|
|
@@ -652,6 +670,53 @@
|
|
|
652
670
|
}
|
|
653
671
|
}
|
|
654
672
|
|
|
673
|
+
/* ——————————————————— mobile / touch performance ——————————————————
|
|
674
|
+
Phones can't sustain the per-frame repaint work: SVG dash/scale
|
|
675
|
+
animations and background-position sheens repaint every frame, and
|
|
676
|
+
each pass runs through blur/drop-shadow filters. Strip the filters
|
|
677
|
+
and the repaint-driven animations; keep every compositor-friendly
|
|
678
|
+
transform/opacity animation so the page stays alive. */
|
|
679
|
+
@media (max-width: 820px), (pointer: coarse) {
|
|
680
|
+
.orb {
|
|
681
|
+
filter: blur(40px);
|
|
682
|
+
}
|
|
683
|
+
.aurora {
|
|
684
|
+
animation: none;
|
|
685
|
+
}
|
|
686
|
+
.grain {
|
|
687
|
+
display: none;
|
|
688
|
+
}
|
|
689
|
+
.wire,
|
|
690
|
+
.wire.v,
|
|
691
|
+
.wire.m,
|
|
692
|
+
.wire.g {
|
|
693
|
+
filter: none;
|
|
694
|
+
animation: none;
|
|
695
|
+
stroke-dasharray: none;
|
|
696
|
+
stroke-width: 1.2;
|
|
697
|
+
opacity: 0.75;
|
|
698
|
+
}
|
|
699
|
+
.layerA,
|
|
700
|
+
.layerB {
|
|
701
|
+
animation: none;
|
|
702
|
+
}
|
|
703
|
+
.ring {
|
|
704
|
+
animation-duration: 7s;
|
|
705
|
+
}
|
|
706
|
+
.hexring {
|
|
707
|
+
filter: none;
|
|
708
|
+
}
|
|
709
|
+
h1 {
|
|
710
|
+
animation: up 1s 0.2s cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
711
|
+
background-position: 35% 50%;
|
|
712
|
+
filter: drop-shadow(0 0 22px rgba(57, 230, 255, 0.35));
|
|
713
|
+
}
|
|
714
|
+
.btn.primary {
|
|
715
|
+
animation: none;
|
|
716
|
+
background-position: 50% 50%;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
655
720
|
/* ————————————————————————— reduced motion ———————————————————————— */
|
|
656
721
|
@media (prefers-reduced-motion: reduce) {
|
|
657
722
|
*, *::before, *::after {
|