@hviana/sema 0.2.3 → 0.2.5
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 +26 -0
- package/dist/src/geometry.js +32 -3
- package/dist/src/mind/attention.d.ts +246 -7
- package/dist/src/mind/attention.js +771 -173
- package/dist/src/mind/bridge.d.ts +30 -0
- package/dist/src/mind/bridge.js +569 -0
- package/dist/src/mind/index.d.ts +2 -0
- package/dist/src/mind/junction.d.ts +30 -1
- package/dist/src/mind/junction.js +73 -18
- package/dist/src/mind/match.d.ts +15 -2
- package/dist/src/mind/match.js +3 -8
- package/dist/src/mind/mechanisms/cast.d.ts +54 -0
- package/dist/src/mind/mechanisms/cast.js +268 -37
- package/dist/src/mind/mechanisms/cover.js +16 -31
- package/dist/src/mind/mechanisms/recall.js +66 -0
- package/dist/src/mind/mind.d.ts +2 -0
- package/dist/src/mind/rationale.d.ts +7 -2
- package/dist/src/mind/rationale.js +6 -5
- package/dist/src/mind/reasoning.d.ts +11 -0
- package/dist/src/mind/reasoning.js +58 -2
- package/dist/src/mind/recognition.js +127 -7
- package/dist/src/mind/traverse.js +90 -25
- package/dist/src/mind/types.d.ts +57 -2
- package/dist/src/mind/types.js +38 -7
- package/dist/src/store.d.ts +12 -3
- package/dist/src/store.js +9 -3
- package/package.json +1 -1
- package/src/geometry.ts +52 -3
- package/src/mind/attention.ts +1199 -128
- package/src/mind/bridge.ts +596 -0
- package/src/mind/index.ts +16 -0
- package/src/mind/junction.ts +125 -16
- package/src/mind/match.ts +19 -5
- package/src/mind/mechanisms/cast.ts +290 -38
- package/src/mind/mechanisms/cover.ts +23 -36
- package/src/mind/mechanisms/recall.ts +79 -0
- package/src/mind/mind.ts +15 -0
- package/src/mind/rationale.ts +12 -4
- package/src/mind/reasoning.ts +71 -2
- package/src/mind/recognition.ts +132 -7
- package/src/mind/traverse.ts +91 -24
- package/src/mind/types.ts +95 -6
- package/src/store.ts +19 -5
- package/test/36-already-answered-fusion.test.mjs +128 -0
- package/test/37-cluster-dispersion-fusion.test.mjs +190 -0
- package/test/38-reason-restate-guard.test.mjs +94 -0
- package/test/39-cast-restate-guard.test.mjs +102 -0
- package/test/40-choosenext-scale-guard.test.mjs +75 -0
- package/test/41-seatofnode-direction.test.mjs +85 -0
- package/test/42-recognise-trace-idempotence.test.mjs +106 -0
- package/test/43-cast-analog-seat.test.mjs +244 -0
- package/test/44-recognise-edge-whitespace.test.mjs +63 -0
- package/test/45-liftanswer-restated-trim.test.mjs +60 -0
- package/test/46-recognise-multibyte-edge.test.mjs +85 -0
- package/test/47-cast-comparison-coverage.test.mjs +134 -0
- package/test/48-recognise-turn-connective.test.mjs +125 -0
- package/test/49-natural-units-synonym-bridge.test.mjs +175 -0
- package/test/50-cast-analog-consensus-floor.test.mjs +179 -0
- package/test/51-structural-resonance-ladder.test.mjs +552 -0
- package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
// The PROJECTIONS built on these walks (follow, conceptHop, reverseContext,
|
|
7
7
|
// project) live in match.ts — the elementary match-and-project operation.
|
|
8
8
|
import { cosine } from "../vec.js";
|
|
9
|
-
import { consensusFloor } from "../geometry.js";
|
|
10
9
|
import { gistOf, read } from "./primitives.js";
|
|
11
10
|
const structCaches = new WeakMap();
|
|
12
11
|
function getStructCache(ctx) {
|
|
@@ -90,7 +89,22 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
90
89
|
// is withdrawn. On a small store the floor stays ≤ √N and the atom
|
|
91
90
|
// climbs exactly as before, so single-letter facts keep working.
|
|
92
91
|
if (id < 0 && atomIsHub(ctx, contextCount)) {
|
|
93
|
-
const
|
|
92
|
+
const bound0 = Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
93
|
+
const reach = {
|
|
94
|
+
roots: [],
|
|
95
|
+
contextsReached: 0,
|
|
96
|
+
saturated: true,
|
|
97
|
+
...(ctx.trace
|
|
98
|
+
? {
|
|
99
|
+
saturation: {
|
|
100
|
+
reason: "byte-atom-commonality",
|
|
101
|
+
node: id,
|
|
102
|
+
observed: atomReach(ctx, contextCount),
|
|
103
|
+
limit: bound0,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
107
|
+
};
|
|
94
108
|
memo?.set(id, reach);
|
|
95
109
|
return reach;
|
|
96
110
|
}
|
|
@@ -99,6 +113,10 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
99
113
|
const seen = new Set([id]);
|
|
100
114
|
const ctxSeen = new Set();
|
|
101
115
|
let saturated = false;
|
|
116
|
+
// Provenance of the FIRST decision that saturated this climb — allocated
|
|
117
|
+
// only when a trace is requested (see AncestorReach.saturation's doc); the
|
|
118
|
+
// climb itself never reads it back.
|
|
119
|
+
let satStop;
|
|
102
120
|
// EXPAND-UNTIL-DECIDED: a reach is consumed either as a VOTE (which needs
|
|
103
121
|
// contextsReached exactly, and only while ≤ √N — beyond that the region is
|
|
104
122
|
// non-discriminative) or as an ABSTENTION (saturated — whose roots and
|
|
@@ -140,16 +158,46 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
140
158
|
roots.push(x);
|
|
141
159
|
if (hasNx)
|
|
142
160
|
ctxSeen.add(x);
|
|
143
|
-
if (pc > bound)
|
|
144
|
-
|
|
161
|
+
if (pc > bound) {
|
|
162
|
+
// decided: ≥ pc > √N distinct contexts
|
|
163
|
+
if (ctx.trace) {
|
|
164
|
+
satStop = {
|
|
165
|
+
reason: "predecessor-fan-in",
|
|
166
|
+
node: x,
|
|
167
|
+
observed: pc,
|
|
168
|
+
limit: bound,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
145
173
|
for (const p of ctx.store.prevFirst(x, bound))
|
|
146
174
|
ctxSeen.add(p);
|
|
147
|
-
if (ctxSeen.size > bound)
|
|
148
|
-
|
|
175
|
+
if (ctxSeen.size > bound) {
|
|
176
|
+
// decided
|
|
177
|
+
if (ctx.trace) {
|
|
178
|
+
satStop = {
|
|
179
|
+
reason: "distinct-context-limit",
|
|
180
|
+
node: x,
|
|
181
|
+
observed: ctxSeen.size,
|
|
182
|
+
limit: bound,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
149
187
|
}
|
|
150
188
|
const parents = ctx.store.parentsFirst(x, bound + 1);
|
|
151
|
-
if (parents.length > bound)
|
|
152
|
-
|
|
189
|
+
if (parents.length > bound) {
|
|
190
|
+
// decided: hub
|
|
191
|
+
if (ctx.trace) {
|
|
192
|
+
satStop = {
|
|
193
|
+
reason: "parent-fan-out",
|
|
194
|
+
node: x,
|
|
195
|
+
observed: parents.length,
|
|
196
|
+
limit: bound,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
153
201
|
let fresh = 0;
|
|
154
202
|
for (const p of parents) {
|
|
155
203
|
if (!seen.has(p)) {
|
|
@@ -160,8 +208,18 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
160
208
|
}
|
|
161
209
|
if (fresh > 1) {
|
|
162
210
|
lateral += fresh - 1;
|
|
163
|
-
if (lateral > bound)
|
|
164
|
-
|
|
211
|
+
if (lateral > bound) {
|
|
212
|
+
// decided: cone-wide hub
|
|
213
|
+
if (ctx.trace) {
|
|
214
|
+
satStop = {
|
|
215
|
+
reason: "lateral-cone-limit",
|
|
216
|
+
node: x,
|
|
217
|
+
observed: lateral,
|
|
218
|
+
limit: bound,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
165
223
|
}
|
|
166
224
|
return true;
|
|
167
225
|
};
|
|
@@ -226,7 +284,12 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
226
284
|
}
|
|
227
285
|
}
|
|
228
286
|
}
|
|
229
|
-
const reach = {
|
|
287
|
+
const reach = {
|
|
288
|
+
roots,
|
|
289
|
+
contextsReached: ctxSeen.size,
|
|
290
|
+
saturated,
|
|
291
|
+
...(saturated && satStop ? { saturation: satStop } : {}),
|
|
292
|
+
};
|
|
230
293
|
memo?.set(id, reach);
|
|
231
294
|
return reach;
|
|
232
295
|
}
|
|
@@ -440,20 +503,22 @@ export function chooseNext(ctx, id, guide) {
|
|
|
440
503
|
bestMass = mass;
|
|
441
504
|
}
|
|
442
505
|
}
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
506
|
+
// NO consensusFloor gate here (tried and reverted — see
|
|
507
|
+
// test/40-choosenext-scale-guard.test.mjs): that floor is calibrated for
|
|
508
|
+
// POOLED, IDF-weighted CLIMB VOTES (recallByResonance, commitVotes), where
|
|
509
|
+
// each corroborating region contributes at most ln N and the floor grows
|
|
510
|
+
// with N exactly as that per-region ceiling does (HOW_IT_WORKS.md §8.6).
|
|
511
|
+
// `bestSupport` here is a different kind of quantity — a raw prevCount of
|
|
512
|
+
// how many training contexts predicted ONE destination, bounded by how
|
|
513
|
+
// often that specific fact was retold, never by corpus size N. Gating an
|
|
514
|
+
// N-invariant count against an N-growing threshold guarantees failure
|
|
515
|
+
// once N is large enough, discarding genuinely, structurally dominant
|
|
516
|
+
// edges (observed: a fact corroborated 2-to-1-1-1 refused at N≈325K,
|
|
517
|
+
// falling back to a noisy concept-hop). The loop above already IS the
|
|
518
|
+
// "genuinely competing" test: a tie leaves first-inserted as the pick
|
|
519
|
+
// (test/30's own pinned behaviour); a strict winner is real evidence
|
|
520
|
+
// regardless of corpus scale. Matches HOW_IT_WORKS.md §25's own
|
|
521
|
+
// chooseNext pseudocode, which has no such floor.
|
|
457
522
|
// Trace is built lazily — the filter + map below only execute when a
|
|
458
523
|
// trace listener is attached, so the common (no-trace) path pays only
|
|
459
524
|
// for the prevCount calls in the loop above, never for extra rItemShort
|
package/dist/src/mind/types.d.ts
CHANGED
|
@@ -74,6 +74,23 @@ export interface Attention {
|
|
|
74
74
|
* consensus; one that does not is a coincidental single-region echo —
|
|
75
75
|
* see test/35-attention-confidence.test.mjs. */
|
|
76
76
|
breadth: number;
|
|
77
|
+
/** DISPERSION: the number of distinct clusters this point's contributing
|
|
78
|
+
* regions form, merging any two whose gap is under one river-fold
|
|
79
|
+
* quantum W. Neither breadth NOR raw region count discriminates a
|
|
80
|
+
* genuine further topic from a coincidental echo (both were tried and
|
|
81
|
+
* falsified — breadth starves a genuine, evenly-split multi-topic query,
|
|
82
|
+
* since no root in a real N-way split can exceed half the vote; raw
|
|
83
|
+
* count doesn't separate them either, since a short, structurally simple
|
|
84
|
+
* echo racks up as many corroborating regions as a real topic does).
|
|
85
|
+
* Dispersion asks a different question: not how MUCH evidence, but how
|
|
86
|
+
* many separate PLACES in the query corroborate it. A coincidental
|
|
87
|
+
* match — one local phrase resonating with an unrelated stored form —
|
|
88
|
+
* is structurally confined to ONE cluster no matter how strong its vote;
|
|
89
|
+
* a genuine further topic is named in its own distinctive wording
|
|
90
|
+
* somewhere the query's scaffolding does not reach, always a SEPARATE
|
|
91
|
+
* cluster from whatever else corroborates it. See
|
|
92
|
+
* test/37-cluster-dispersion-fusion.test.mjs. */
|
|
93
|
+
clusters: number;
|
|
77
94
|
}
|
|
78
95
|
/** Both read-outs of one consensus climb. */
|
|
79
96
|
export interface AttentionRead {
|
|
@@ -116,11 +133,29 @@ export interface RegionVote {
|
|
|
116
133
|
* Defaults to 1 when absent. */
|
|
117
134
|
absorbed?: number;
|
|
118
135
|
}
|
|
136
|
+
/** The structural gate that first decided an {@link edgeAncestors} climb was
|
|
137
|
+
* saturated (an abstention, not a discriminative conclusion) — pure
|
|
138
|
+
* instrumentation for {@link ClimbConsensusData}'s reach trace; it never
|
|
139
|
+
* feeds back into the climb itself. */
|
|
140
|
+
export type SaturationReason = "byte-atom-commonality" | "predecessor-fan-in" | "distinct-context-limit" | "parent-fan-out" | "lateral-cone-limit";
|
|
141
|
+
/** One saturation stop's provenance: which reason fired, at which node, the
|
|
142
|
+
* observed count against the bound that decided it. */
|
|
143
|
+
export interface SaturationStop {
|
|
144
|
+
reason: SaturationReason;
|
|
145
|
+
node: number;
|
|
146
|
+
observed: number;
|
|
147
|
+
limit: number;
|
|
148
|
+
}
|
|
119
149
|
/** The edge-bearing contexts reached by climbing from a node, plus saturation info. */
|
|
120
150
|
export interface AncestorReach {
|
|
121
151
|
roots: number[];
|
|
122
152
|
contextsReached: number;
|
|
123
153
|
saturated: boolean;
|
|
154
|
+
/** The saturation gate that stopped this climb, when {@link saturated} is
|
|
155
|
+
* true and a trace was requested — see {@link edgeAncestors}. Absent for
|
|
156
|
+
* a non-saturated reach, and absent (even when saturated) when no trace
|
|
157
|
+
* was requested — instrumentation must not allocate when tracing is off. */
|
|
158
|
+
saturation?: SaturationStop;
|
|
124
159
|
}
|
|
125
160
|
/** Saturated-interval information for the noise-drop gate. */
|
|
126
161
|
export interface SaturationInfo {
|
|
@@ -226,9 +261,29 @@ export interface MindContext extends GraphSearchHost {
|
|
|
226
261
|
export declare const ALL = 2147483647;
|
|
227
262
|
/** Splice every chosen span in order — the whole cover as one byte string. */
|
|
228
263
|
export declare function spliceAll(segs: Seg[]): Uint8Array | null;
|
|
264
|
+
/** Whether a chosen span RESTATES the query rather than answering it: its
|
|
265
|
+
* SUBSTITUTED bytes (an edge followed from a recognised site, not the
|
|
266
|
+
* site's own literal text read back) already occur elsewhere in the query
|
|
267
|
+
* — the same principle recall.ts's tiers apply to a whole-query projection
|
|
268
|
+
* ("a projection that is a proper byte-subspan of the query restates part
|
|
269
|
+
* of the question"). A LITERAL span (the site's own bytes, unchanged) is
|
|
270
|
+
* exempt: naming what's already there at its OWN position is not a
|
|
271
|
+
* substitution. A recognised site that is itself an entire PRIOR TURN of
|
|
272
|
+
* a multi-turn query is exactly this shape: it carries a genuine learnt
|
|
273
|
+
* continuation, but that continuation is something the asker already said
|
|
274
|
+
* moments later in the SAME query, not a new answer. Below one river
|
|
275
|
+
* window, byte overlap is chance, not evidence — the same floor
|
|
276
|
+
* identityBar and reachThreshold hold every other structural-overlap claim
|
|
277
|
+
* to. */
|
|
278
|
+
export declare function segRestatesQuery(s: Seg, query: Uint8Array, queryLen: number, W: number): boolean;
|
|
229
279
|
/** Lift the answer out of the cover for think: the recognised region, free of
|
|
230
|
-
* the asker's surrounding (unrecognised) framing
|
|
231
|
-
|
|
280
|
+
* the asker's surrounding (unrecognised) framing — and free of any chosen
|
|
281
|
+
* span that only RESTATES content the query already contains (see {@link
|
|
282
|
+
* segRestatesQuery}). A restating span is excluded from both the framing
|
|
283
|
+
* (lo/hi) decision and the final concatenation: it is stale, not a second
|
|
284
|
+
* answer, but the OTHER spans a derivation chose are independent evidence
|
|
285
|
+
* and must not be discarded along with it. */
|
|
286
|
+
export declare function liftAnswer(segs: Seg[], queryLen: number, query: Uint8Array, W: number): Uint8Array | null;
|
|
232
287
|
/** The CHANGED NODES of a freshly-perceived `tree` against the node ids a previous
|
|
233
288
|
* tracked deposit interned (`prevSeen`). */
|
|
234
289
|
export declare function changedNodes(tree: Sema, ids: Map<Sema, number>, prevSeen: Set<number>): Sema[];
|
package/dist/src/mind/types.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// GraphSearchHost is defined first (minimal imports) so GraphSearch can import
|
|
4
4
|
// it without pulling in the full MindContext.
|
|
5
|
-
import { concatBytes } from "../bytes.js";
|
|
5
|
+
import { bytesEqual, concatBytes, indexOf } from "../bytes.js";
|
|
6
6
|
import { dominates } from "../geometry.js";
|
|
7
7
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
8
|
// FREE FUNCTIONS (pure, no state)
|
|
@@ -15,13 +15,44 @@ export function spliceAll(segs) {
|
|
|
15
15
|
return null;
|
|
16
16
|
return concatBytes(segs.map((s) => s.bytes));
|
|
17
17
|
}
|
|
18
|
+
/** Whether a chosen span RESTATES the query rather than answering it: its
|
|
19
|
+
* SUBSTITUTED bytes (an edge followed from a recognised site, not the
|
|
20
|
+
* site's own literal text read back) already occur elsewhere in the query
|
|
21
|
+
* — the same principle recall.ts's tiers apply to a whole-query projection
|
|
22
|
+
* ("a projection that is a proper byte-subspan of the query restates part
|
|
23
|
+
* of the question"). A LITERAL span (the site's own bytes, unchanged) is
|
|
24
|
+
* exempt: naming what's already there at its OWN position is not a
|
|
25
|
+
* substitution. A recognised site that is itself an entire PRIOR TURN of
|
|
26
|
+
* a multi-turn query is exactly this shape: it carries a genuine learnt
|
|
27
|
+
* continuation, but that continuation is something the asker already said
|
|
28
|
+
* moments later in the SAME query, not a new answer. Below one river
|
|
29
|
+
* window, byte overlap is chance, not evidence — the same floor
|
|
30
|
+
* identityBar and reachThreshold hold every other structural-overlap claim
|
|
31
|
+
* to. */
|
|
32
|
+
export function segRestatesQuery(s, query, queryLen, W) {
|
|
33
|
+
if (!s.rec)
|
|
34
|
+
return false;
|
|
35
|
+
const literal = s.j - s.i === s.bytes.length &&
|
|
36
|
+
bytesEqual(s.bytes, query.subarray(s.i, s.j));
|
|
37
|
+
if (literal)
|
|
38
|
+
return false;
|
|
39
|
+
return s.bytes.length >= W && s.bytes.length < queryLen &&
|
|
40
|
+
indexOf(query, s.bytes, 0) >= 0;
|
|
41
|
+
}
|
|
18
42
|
/** Lift the answer out of the cover for think: the recognised region, free of
|
|
19
|
-
* the asker's surrounding (unrecognised) framing
|
|
20
|
-
|
|
43
|
+
* the asker's surrounding (unrecognised) framing — and free of any chosen
|
|
44
|
+
* span that only RESTATES content the query already contains (see {@link
|
|
45
|
+
* segRestatesQuery}). A restating span is excluded from both the framing
|
|
46
|
+
* (lo/hi) decision and the final concatenation: it is stale, not a second
|
|
47
|
+
* answer, but the OTHER spans a derivation chose are independent evidence
|
|
48
|
+
* and must not be discarded along with it. */
|
|
49
|
+
export function liftAnswer(segs, queryLen, query, W) {
|
|
50
|
+
const restated = segs.map((s) => segRestatesQuery(s, query, queryLen, W));
|
|
21
51
|
const recognised = [];
|
|
22
|
-
for (let k = 0; k < segs.length; k++)
|
|
23
|
-
if (segs[k].rec)
|
|
52
|
+
for (let k = 0; k < segs.length; k++) {
|
|
53
|
+
if (segs[k].rec && !restated[k])
|
|
24
54
|
recognised.push(k);
|
|
55
|
+
}
|
|
25
56
|
if (recognised.length === 0)
|
|
26
57
|
return null;
|
|
27
58
|
if (recognised.length === 1) {
|
|
@@ -42,13 +73,13 @@ export function liftAnswer(segs, queryLen) {
|
|
|
42
73
|
if (s.computed && s.i > 0)
|
|
43
74
|
return s.bytes;
|
|
44
75
|
if (dominates(s.j - s.i, queryLen)) {
|
|
45
|
-
return concatBytes(segs.map((x) => x.bytes));
|
|
76
|
+
return concatBytes(segs.filter((_, k) => !restated[k]).map((x) => x.bytes));
|
|
46
77
|
}
|
|
47
78
|
return s.bytes;
|
|
48
79
|
}
|
|
49
80
|
const lo = recognised[0];
|
|
50
81
|
const hi = recognised[recognised.length - 1];
|
|
51
|
-
return concatBytes(segs.slice(lo, hi + 1).map((x) => x.bytes));
|
|
82
|
+
return concatBytes(segs.slice(lo, hi + 1).filter((_, k) => !restated[lo + k]).map((x) => x.bytes));
|
|
52
83
|
}
|
|
53
84
|
/** The CHANGED NODES of a freshly-perceived `tree` against the node ids a previous
|
|
54
85
|
* tracked deposit interned (`prevSeen`). */
|
package/dist/src/store.d.ts
CHANGED
|
@@ -139,8 +139,17 @@ export interface Store {
|
|
|
139
139
|
* question is decided, with per-page work bounded by `limit`. */
|
|
140
140
|
containersSlice(child: NodeId, offset: number, limit: number): NodeId[];
|
|
141
141
|
nodeCount(): number;
|
|
142
|
-
/** The k nodes whose gist resonates most with v.
|
|
143
|
-
|
|
142
|
+
/** The k nodes whose gist resonates most with v. `exhaustive` widens the
|
|
143
|
+
* IVF probe to every cluster (see {@link AbstractStore.efFor}'s doc) —
|
|
144
|
+
* for refusal-path-only callers where an approximate top-√C-clusters
|
|
145
|
+
* search is not the same discriminator as the caller's own byte-exact
|
|
146
|
+
* verification (the substitution bridge's proposal channel): a rarer
|
|
147
|
+
* paraphrase can score lower than hundreds of unrelated hits by pure
|
|
148
|
+
* fold-geometry structural distance (a middle-of-string mismatch
|
|
149
|
+
* perturbs the tree hash far more than a tail mismatch of the same
|
|
150
|
+
* byte length) and so never even reach a probed cluster, no matter how
|
|
151
|
+
* large k is — k only reorders WITHIN the clusters already probed. */
|
|
152
|
+
resonate(v: Vec, k: number, exhaustive?: boolean): Promise<Hit[]>;
|
|
144
153
|
/** Mark a node as a RESONANCE TARGET — promote its gist into the content
|
|
145
154
|
* index so {@link resonate} can find it. A node's gist is captured at intern
|
|
146
155
|
* but indexed LAZILY (only targets are indexed; the ~99.5% intermediate DAG
|
|
@@ -593,7 +602,7 @@ export declare abstract class AbstractStore implements Store {
|
|
|
593
602
|
*
|
|
594
603
|
* Iterative explicit-queue walk: the call stack never sees tree depth. */
|
|
595
604
|
protected indexSubtree(root: NodeId): void;
|
|
596
|
-
resonate(v: Vec, k: number): Promise<Hit[]>;
|
|
605
|
+
resonate(v: Vec, k: number, exhaustive?: boolean): Promise<Hit[]>;
|
|
597
606
|
indexedVectorCount(): number;
|
|
598
607
|
lastResonateReads(): number;
|
|
599
608
|
/** How many physical compaction attempts have failed this session. Zero in
|
package/dist/src/store.js
CHANGED
|
@@ -1135,7 +1135,7 @@ export class AbstractStore {
|
|
|
1135
1135
|
}
|
|
1136
1136
|
}
|
|
1137
1137
|
// ── Soft resonance ─────────────────────────────────────────────────────
|
|
1138
|
-
async resonate(v, k) {
|
|
1138
|
+
async resonate(v, k, exhaustive = false) {
|
|
1139
1139
|
await this._ensureReady();
|
|
1140
1140
|
// Synchronous flush of any buffered index writes: the FIRST resonance
|
|
1141
1141
|
// after a large ingest pays that flush here, so it shows up in respond
|
|
@@ -1148,14 +1148,20 @@ export class AbstractStore {
|
|
|
1148
1148
|
// same values still hits. Lazy-init: null after any index write; the
|
|
1149
1149
|
// first miss after a flush recreates it. When voteRegions resonates
|
|
1150
1150
|
// identical perceived sub-regions, only the first call descends the ANN.
|
|
1151
|
-
const rk = vecKey(v) + ":" + k;
|
|
1151
|
+
const rk = vecKey(v) + ":" + k + (exhaustive ? ":x" : "");
|
|
1152
1152
|
const cache = this._resonateCache;
|
|
1153
1153
|
if (cache) {
|
|
1154
1154
|
const hit = cache.get(rk);
|
|
1155
1155
|
if (hit !== undefined)
|
|
1156
1156
|
return hit;
|
|
1157
1157
|
}
|
|
1158
|
-
const
|
|
1158
|
+
const clusters = this._vecContentClusterCount();
|
|
1159
|
+
const results = this._vecContentQuery(normalize(copy(v)), k * this.overfetch,
|
|
1160
|
+
// Exhaustive: probe every cluster (ef ≥ 4·clusters guarantees the
|
|
1161
|
+
// IVF's own ef→nprobe=ceil(ef/4) mapping reaches all of them) — the
|
|
1162
|
+
// natural ceiling for a search that is ALREADY refusal-path-only and
|
|
1163
|
+
// must not miss a candidate hiding in an unprobed cluster.
|
|
1164
|
+
exhaustive ? 4 * clusters : this.efFor(clusters));
|
|
1159
1165
|
const out = [];
|
|
1160
1166
|
for (const r of results) {
|
|
1161
1167
|
const id = r.id;
|
package/package.json
CHANGED
package/src/geometry.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// identically regardless of what follows — pure structural stability.
|
|
8
8
|
// 3. The same rule recurses level after level until one root remains.
|
|
9
9
|
|
|
10
|
-
import { normalize, Vec } from "./vec.js";
|
|
10
|
+
import { addInto, copy, normalize, Vec, zeros } from "./vec.js";
|
|
11
11
|
import { Sema, sema, Space } from "./sema.js";
|
|
12
12
|
import { Alphabet } from "./alphabet.js";
|
|
13
13
|
|
|
@@ -461,8 +461,10 @@ function fold2(space: Space, a: Folded, b: Folded): Folded {
|
|
|
461
461
|
/** Plain river fold WITHOUT the final root normalize — the segment-level
|
|
462
462
|
* building block of {@link stablePrefixFold} (interiors must keep their
|
|
463
463
|
* byte-proportional magnitude; only the whole perception's root is ever
|
|
464
|
-
* normalized).
|
|
465
|
-
|
|
464
|
+
* normalized). Exported so callers that COMPOSE already-existing structural
|
|
465
|
+
* parts into a hypothetical synthetic root (see {@link composeStructuralGist})
|
|
466
|
+
* can feed the same raw primitive instead of duplicating its mathematics. */
|
|
467
|
+
export function riverFoldRaw(space: Space, row: Folded[]): Folded {
|
|
466
468
|
if (row.length === 0) {
|
|
467
469
|
const z = new Float32Array(space.D);
|
|
468
470
|
return { tree: sema(z, new Uint8Array(0), null), len: 0 };
|
|
@@ -477,6 +479,53 @@ function riverFoldRaw(space: Space, row: Folded[]): Folded {
|
|
|
477
479
|
return level[0];
|
|
478
480
|
}
|
|
479
481
|
|
|
482
|
+
// ---- structural composition (synthesize from EXISTING structural parts) ----
|
|
483
|
+
|
|
484
|
+
/** One already-existing structural vector to compose, paired with the byte
|
|
485
|
+
* span (query-slot) length it stands in for. `len`, not the vector's own
|
|
486
|
+
* magnitude, is what {@link composeStructuralGist} restores — the composed
|
|
487
|
+
* slot's NATURAL span, exactly as the linear river fold would carry it. */
|
|
488
|
+
export interface StructuralPart {
|
|
489
|
+
v: Vec;
|
|
490
|
+
len: number;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Synthesize a hypothetical internal structure from already-existing
|
|
494
|
+
* structural vectors — NOT from bytes. This is the raw positional
|
|
495
|
+
* composition the linear river fold already uses (see the folding header
|
|
496
|
+
* above): each part is positionally bound into its own seat, its natural
|
|
497
|
+
* span magnitude is preserved, the parts are linearly superposed, and only
|
|
498
|
+
* the final synthetic root is normalized. It never calls {@link gistOf}
|
|
499
|
+
* (there is no `gistOf` here — geometry.ts has no store), never perceives a
|
|
500
|
+
* concatenated byte string, and never interns or stores a new node: the
|
|
501
|
+
* result is an opaque, ungrounded Vec for an ANN probe only. */
|
|
502
|
+
export function composeStructuralGist(
|
|
503
|
+
space: Space,
|
|
504
|
+
parts: readonly StructuralPart[],
|
|
505
|
+
): Vec {
|
|
506
|
+
const foldedParts: Folded[] = [];
|
|
507
|
+
|
|
508
|
+
for (const part of parts) {
|
|
509
|
+
if (part.len <= 0) continue;
|
|
510
|
+
|
|
511
|
+
const direction = copy(part.v);
|
|
512
|
+
normalize(direction);
|
|
513
|
+
|
|
514
|
+
const scaled = zeros(space.D);
|
|
515
|
+
addInto(scaled, direction, Math.sqrt(part.len));
|
|
516
|
+
|
|
517
|
+
foldedParts.push({ tree: sema(scaled), len: part.len });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (foldedParts.length === 0) return zeros(space.D);
|
|
521
|
+
|
|
522
|
+
const rawRoot = riverFoldRaw(space, foldedParts);
|
|
523
|
+
|
|
524
|
+
const result = copy(rawRoot.tree.v);
|
|
525
|
+
normalize(result);
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
|
|
480
529
|
// ---- pyramid fold (incremental plain perception) ----
|
|
481
530
|
|
|
482
531
|
/** The PLAIN fold's full level pyramid — every level's item list, bottom
|