mumpix 1.0.20 → 1.1.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/CHANGELOG.md +82 -11
- package/README.md +278 -8
- package/bin/mumpix.js +1 -405
- package/examples/agent-memory.js +1 -1
- package/examples/basic.js +1 -1
- package/examples/behavioral-primitives.js +50 -0
- package/examples/recall-quality-suite.js +156 -0
- package/examples/verified-mode.js +1 -1
- package/package.json +24 -13
- package/scripts/bench-semantic.cjs +76 -0
- package/scripts/test-license-modes.cjs +87 -0
- package/scripts/test-phase0.cjs +137 -0
- package/scripts/test-semantic.cjs +411 -0
- package/src/brp/index.js +1 -0
- package/src/collapse/index.js +1 -0
- package/src/core/MumpixDB.js +264 -323
- package/src/core/audit.js +1 -173
- package/src/core/auth.js +1 -232
- package/src/core/inverted-index.js +249 -0
- package/src/core/license.js +1 -267
- package/src/core/ml-dsa.mjs +1 -25
- package/src/core/ml-kem.mjs +1 -32
- package/src/core/recall.js +226 -112
- package/src/core/semantic-sidecar.js +312 -0
- package/src/core/store.js +365 -288
- package/src/core/wal-writer.js +83 -0
- package/src/index.js +20 -34
- package/src/integrations/developer-sdk.js +1 -165
- package/src/integrations/langchain-official.js +1 -0
- package/src/integrations/langchain.js +1 -131
- package/src/integrations/llamaindex-official.js +1 -0
- package/src/integrations/llamaindex.js +1 -86
- package/src/integrations/vector-sidecar.js +325 -0
- package/src/rlp/index.js +1 -0
- package/src/temporal/engine.js +1 -1894
- package/src/temporal/indexes.js +1 -178
- package/src/temporal/operators.js +1 -186
- package/scripts/postinstall-auth.js +0 -101
package/src/core/recall.js
CHANGED
|
@@ -1,176 +1,290 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
* Strategy (in order):
|
|
7
|
-
* 1. Exact substring match (zero-latency)
|
|
8
|
-
* 2. TF-IDF cosine similarity (local semantic approximation, no API needed)
|
|
9
|
-
* 3. Token overlap fallback (always produces a result)
|
|
10
|
-
*
|
|
11
|
-
* Optional: pass embedFn to use your own embeddings (OpenAI, Cohere, etc.)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
// ── Stopwords ────────────────────────────────────
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MIN_SCORE = 0.15;
|
|
4
|
+
|
|
15
5
|
const STOPWORDS = new Set([
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
6
|
+
"a",
|
|
7
|
+
"an",
|
|
8
|
+
"the",
|
|
9
|
+
"is",
|
|
10
|
+
"are",
|
|
11
|
+
"was",
|
|
12
|
+
"were",
|
|
13
|
+
"be",
|
|
14
|
+
"been",
|
|
15
|
+
"being",
|
|
16
|
+
"have",
|
|
17
|
+
"has",
|
|
18
|
+
"had",
|
|
19
|
+
"do",
|
|
20
|
+
"does",
|
|
21
|
+
"did",
|
|
22
|
+
"will",
|
|
23
|
+
"would",
|
|
24
|
+
"could",
|
|
25
|
+
"should",
|
|
26
|
+
"may",
|
|
27
|
+
"might",
|
|
28
|
+
"i",
|
|
29
|
+
"you",
|
|
30
|
+
"he",
|
|
31
|
+
"she",
|
|
32
|
+
"it",
|
|
33
|
+
"we",
|
|
34
|
+
"they",
|
|
35
|
+
"my",
|
|
36
|
+
"your",
|
|
37
|
+
"his",
|
|
38
|
+
"her",
|
|
39
|
+
"its",
|
|
40
|
+
"our",
|
|
41
|
+
"their",
|
|
42
|
+
"what",
|
|
43
|
+
"which",
|
|
44
|
+
"who",
|
|
45
|
+
"whom",
|
|
46
|
+
"that",
|
|
47
|
+
"this",
|
|
48
|
+
"these",
|
|
49
|
+
"those",
|
|
50
|
+
"and",
|
|
51
|
+
"but",
|
|
52
|
+
"or",
|
|
53
|
+
"nor",
|
|
54
|
+
"for",
|
|
55
|
+
"so",
|
|
56
|
+
"yet",
|
|
57
|
+
"in",
|
|
58
|
+
"on",
|
|
59
|
+
"at",
|
|
60
|
+
"to",
|
|
61
|
+
"of",
|
|
62
|
+
"up",
|
|
63
|
+
"by",
|
|
64
|
+
"with",
|
|
65
|
+
"about",
|
|
66
|
+
"into",
|
|
67
|
+
"through",
|
|
68
|
+
"during",
|
|
69
|
+
"before",
|
|
70
|
+
"after",
|
|
71
|
+
"above",
|
|
72
|
+
"below",
|
|
73
|
+
"from",
|
|
74
|
+
"out",
|
|
75
|
+
"off",
|
|
76
|
+
"over",
|
|
77
|
+
"under",
|
|
78
|
+
"again",
|
|
79
|
+
"then",
|
|
80
|
+
"once",
|
|
81
|
+
"here",
|
|
82
|
+
"there",
|
|
83
|
+
"when",
|
|
84
|
+
"where",
|
|
85
|
+
"why",
|
|
86
|
+
"how",
|
|
87
|
+
"all",
|
|
88
|
+
"both",
|
|
89
|
+
"each",
|
|
90
|
+
"few",
|
|
91
|
+
"more",
|
|
92
|
+
"most",
|
|
93
|
+
"other",
|
|
94
|
+
"some",
|
|
95
|
+
"such",
|
|
96
|
+
"no",
|
|
97
|
+
"not",
|
|
98
|
+
"only",
|
|
99
|
+
"own",
|
|
100
|
+
"same",
|
|
101
|
+
"than",
|
|
102
|
+
"too",
|
|
103
|
+
"very",
|
|
104
|
+
"just",
|
|
105
|
+
"can",
|
|
106
|
+
"me",
|
|
107
|
+
"him",
|
|
108
|
+
"us",
|
|
109
|
+
"them",
|
|
110
|
+
"am",
|
|
111
|
+
"get",
|
|
112
|
+
"got",
|
|
113
|
+
"put",
|
|
114
|
+
"set",
|
|
115
|
+
"let",
|
|
116
|
+
"if",
|
|
117
|
+
"as",
|
|
118
|
+
"also",
|
|
119
|
+
"even",
|
|
120
|
+
"still",
|
|
121
|
+
"already",
|
|
122
|
+
"now",
|
|
29
123
|
]);
|
|
30
124
|
|
|
31
|
-
// ── TF-IDF utilities ─────────────────────────────
|
|
32
|
-
|
|
33
125
|
function tokenize(text) {
|
|
34
|
-
return text
|
|
126
|
+
return String(text || "")
|
|
35
127
|
.toLowerCase()
|
|
36
|
-
.replace(/[^a-z0-9\s'-]/g,
|
|
128
|
+
.replace(/[^a-z0-9\s'-]/g, " ")
|
|
37
129
|
.split(/\s+/)
|
|
38
|
-
.filter(
|
|
130
|
+
.filter((token) => token.length > 1 && !STOPWORDS.has(token));
|
|
39
131
|
}
|
|
40
132
|
|
|
41
133
|
function tf(tokens) {
|
|
42
134
|
const freq = {};
|
|
43
|
-
for (const
|
|
135
|
+
for (const token of tokens) freq[token] = (freq[token] || 0) + 1;
|
|
136
|
+
|
|
44
137
|
const len = tokens.length || 1;
|
|
45
138
|
const out = {};
|
|
46
|
-
for (const [
|
|
139
|
+
for (const [token, count] of Object.entries(freq)) out[token] = count / len;
|
|
47
140
|
return out;
|
|
48
141
|
}
|
|
49
142
|
|
|
50
143
|
function buildIDF(corpus) {
|
|
51
144
|
const df = {};
|
|
52
|
-
const
|
|
145
|
+
const count = corpus.length;
|
|
146
|
+
|
|
53
147
|
for (const doc of corpus) {
|
|
54
|
-
const
|
|
55
|
-
for (const t of seen) df[t] = (df[t] || 0) + 1;
|
|
148
|
+
for (const token of new Set(doc)) df[token] = (df[token] || 0) + 1;
|
|
56
149
|
}
|
|
150
|
+
|
|
57
151
|
const idf = {};
|
|
58
|
-
for (const [
|
|
59
|
-
idf[
|
|
152
|
+
for (const [token, docCount] of Object.entries(df)) {
|
|
153
|
+
idf[token] = Math.log((count + 1) / (docCount + 1)) + 1;
|
|
60
154
|
}
|
|
61
155
|
return idf;
|
|
62
156
|
}
|
|
63
157
|
|
|
64
158
|
function tfidfVec(tfMap, idf) {
|
|
65
159
|
const vec = {};
|
|
66
|
-
for (const [
|
|
67
|
-
vec[
|
|
160
|
+
for (const [token, weight] of Object.entries(tfMap)) {
|
|
161
|
+
vec[token] = weight * (idf[token] || 1);
|
|
68
162
|
}
|
|
69
163
|
return vec;
|
|
70
164
|
}
|
|
71
165
|
|
|
72
166
|
function cosine(a, b) {
|
|
73
|
-
let dot = 0
|
|
167
|
+
let dot = 0;
|
|
168
|
+
let normA = 0;
|
|
169
|
+
let normB = 0;
|
|
74
170
|
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
171
|
+
|
|
172
|
+
for (const key of keys) {
|
|
173
|
+
const av = a[key] || 0;
|
|
174
|
+
const bv = b[key] || 0;
|
|
175
|
+
dot += av * bv;
|
|
176
|
+
normA += av * av;
|
|
177
|
+
normB += bv * bv;
|
|
81
178
|
}
|
|
179
|
+
|
|
82
180
|
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
83
181
|
return denom === 0 ? 0 : dot / denom;
|
|
84
182
|
}
|
|
85
183
|
|
|
86
|
-
|
|
184
|
+
function cosineArrays(a, b) {
|
|
185
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return 0;
|
|
186
|
+
let dot = 0;
|
|
187
|
+
let normA = 0;
|
|
188
|
+
let normB = 0;
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < a.length; i++) {
|
|
191
|
+
dot += a[i] * b[i];
|
|
192
|
+
normA += a[i] * a[i];
|
|
193
|
+
normB += b[i] * b[i];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
197
|
+
return denom === 0 ? 0 : dot / denom;
|
|
198
|
+
}
|
|
87
199
|
|
|
88
200
|
function tokenOverlap(queryTokens, docTokens) {
|
|
89
201
|
if (!queryTokens.length) return 0;
|
|
90
202
|
const docSet = new Set(docTokens);
|
|
91
|
-
|
|
92
|
-
|
|
203
|
+
return queryTokens.filter((token) => docSet.has(token)).length / queryTokens.length;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function minScore(opts) {
|
|
207
|
+
if (opts.minScore === null || opts.minScore === false) return -Infinity;
|
|
208
|
+
return Number.isFinite(opts.minScore) ? Number(opts.minScore) : DEFAULT_MIN_SCORE;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function sortScored(a, b) {
|
|
212
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
213
|
+
const bTs = Number.isFinite(b.r && b.r.ts) ? b.r.ts : 0;
|
|
214
|
+
const aTs = Number.isFinite(a.r && a.r.ts) ? a.r.ts : 0;
|
|
215
|
+
if (bTs !== aTs) return bTs - aTs;
|
|
216
|
+
const aId = Number.isFinite(a.r && a.r.id) ? a.r.id : 0;
|
|
217
|
+
const bId = Number.isFinite(b.r && b.r.id) ? b.r.id : 0;
|
|
218
|
+
return aId - bId;
|
|
93
219
|
}
|
|
94
220
|
|
|
95
|
-
// ── Main recall function ──────────────────────────
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* recall(query, records, opts) → Record | null
|
|
99
|
-
*
|
|
100
|
-
* opts.k — number of results to return (default 1)
|
|
101
|
-
* opts.embedFn — async fn(texts[]) → number[][] for custom embeddings
|
|
102
|
-
* opts.filter — fn(record) → bool for pre-filtering
|
|
103
|
-
* opts.since — timestamp: only consider records newer than this
|
|
104
|
-
* opts.mode — "exact" | "semantic" | "hybrid" (default "hybrid")
|
|
105
|
-
*/
|
|
106
221
|
async function recall(query, records, opts = {}) {
|
|
107
222
|
const results = await recallMany(query, records, { ...opts, k: opts.k || 1 });
|
|
108
223
|
return results.length ? results[0] : null;
|
|
109
224
|
}
|
|
110
225
|
|
|
111
226
|
async function recallMany(query, records, opts = {}) {
|
|
112
|
-
const k
|
|
113
|
-
const mode
|
|
227
|
+
const k = opts.k || 5;
|
|
228
|
+
const mode = opts.mode || "hybrid";
|
|
114
229
|
const filter = opts.filter || null;
|
|
115
|
-
const since
|
|
230
|
+
const since = opts.since || null;
|
|
231
|
+
const threshold = minScore(opts);
|
|
116
232
|
|
|
117
233
|
let pool = records;
|
|
118
234
|
if (filter) pool = pool.filter(filter);
|
|
119
|
-
if (since)
|
|
235
|
+
if (since) pool = pool.filter((record) => record.ts >= since);
|
|
120
236
|
if (!pool.length) return [];
|
|
121
237
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
238
|
+
const queryLower = String(query || "").toLowerCase();
|
|
239
|
+
if (mode !== "semantic") {
|
|
240
|
+
const exact = pool
|
|
241
|
+
.filter((record) => String(record.content || "").toLowerCase().includes(queryLower))
|
|
242
|
+
.map((record) => ({ ...record, _score: 1 }));
|
|
243
|
+
|
|
244
|
+
if (mode === "exact") return exact.slice(0, k);
|
|
245
|
+
if (exact.length >= k) return exact.slice(0, k);
|
|
128
246
|
}
|
|
129
247
|
|
|
130
|
-
|
|
131
|
-
if (opts.embedFn && mode !== 'exact') {
|
|
248
|
+
if (opts.embedFn && mode !== "exact") {
|
|
132
249
|
try {
|
|
133
|
-
const texts
|
|
134
|
-
const vectors
|
|
135
|
-
const qVec
|
|
136
|
-
const scored
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
250
|
+
const texts = [query, ...pool.map((record) => record.content)];
|
|
251
|
+
const vectors = await opts.embedFn(texts);
|
|
252
|
+
const qVec = vectors[0];
|
|
253
|
+
const scored = pool
|
|
254
|
+
.map((record, index) => ({ r: record, score: cosineArrays(qVec, vectors[index + 1]) }))
|
|
255
|
+
.filter((item) => item.score >= threshold);
|
|
256
|
+
|
|
257
|
+
scored.sort(sortScored);
|
|
258
|
+
return scored.slice(0, k).map((item) => ({ ...item.r, _score: item.score }));
|
|
259
|
+
} catch (_) {
|
|
260
|
+
// Fall through to local TF-IDF.
|
|
261
|
+
}
|
|
140
262
|
}
|
|
141
263
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const docTokens = pool.map(r => tokenize(r.content));
|
|
145
|
-
const corpus = [qTokens, ...docTokens];
|
|
146
|
-
const idf = buildIDF(corpus);
|
|
147
|
-
|
|
148
|
-
const qTF = tf(qTokens);
|
|
149
|
-
const qVec = tfidfVec(qTF, idf);
|
|
150
|
-
|
|
151
|
-
const scored = pool.map((r, i) => {
|
|
152
|
-
const dVec = tfidfVec(tf(docTokens[i]), idf);
|
|
153
|
-
const sem = cosine(qVec, dVec);
|
|
154
|
-
const over = tokenOverlap(qTokens, docTokens[i]);
|
|
155
|
-
// Blend: 70% semantic + 30% overlap, with recency boost
|
|
156
|
-
const recency = Math.exp(-(Date.now() - r.ts) / (1000 * 60 * 60 * 24 * 7)); // 7-day half-life
|
|
157
|
-
const score = (sem * 0.70) + (over * 0.20) + (recency * 0.10);
|
|
158
|
-
return { r, score, _debug: { sem, over, recency } };
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
scored.sort((a, b) => b.score - a.score);
|
|
162
|
-
return scored.slice(0, k).map(s => ({ ...s.r, _score: s.score }));
|
|
163
|
-
}
|
|
264
|
+
const qTokens = tokenize(query);
|
|
265
|
+
if (!qTokens.length) return [];
|
|
164
266
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
267
|
+
const docTokens = pool.map((record) => tokenize(record.content));
|
|
268
|
+
const idf = buildIDF([qTokens, ...docTokens]);
|
|
269
|
+
const qVec = tfidfVec(tf(qTokens), idf);
|
|
270
|
+
|
|
271
|
+
const scored = pool
|
|
272
|
+
.map((record, index) => {
|
|
273
|
+
const dVec = tfidfVec(tf(docTokens[index]), idf);
|
|
274
|
+
const semantic = cosine(qVec, dVec);
|
|
275
|
+
const overlap = tokenOverlap(qTokens, docTokens[index]);
|
|
276
|
+
const related = semantic > 0 || overlap > 0;
|
|
277
|
+
const recency = related
|
|
278
|
+
? Math.exp(-(Date.now() - record.ts) / (1000 * 60 * 60 * 24 * 7))
|
|
279
|
+
: 0;
|
|
280
|
+
const score = semantic * 0.7 + overlap * 0.2 + recency * 0.1;
|
|
281
|
+
|
|
282
|
+
return { r: record, score, _debug: { semantic, overlap, recency } };
|
|
283
|
+
})
|
|
284
|
+
.filter((item) => item.score >= threshold);
|
|
285
|
+
|
|
286
|
+
scored.sort(sortScored);
|
|
287
|
+
return scored.slice(0, k).map((item) => ({ ...item.r, _score: item.score }));
|
|
174
288
|
}
|
|
175
289
|
|
|
176
290
|
module.exports = { recall, recallMany, tokenize };
|