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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
function vectorPathForDb(dbPath) {
|
|
7
|
+
return `${dbPath}.vectors.json`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function vectorLogPathForSnapshot(vectorPath) {
|
|
11
|
+
return `${vectorPath}l`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function emptySidecar(embedder) {
|
|
15
|
+
return {
|
|
16
|
+
version: 1,
|
|
17
|
+
embedder_id: embedder.id,
|
|
18
|
+
dim: embedder.dim,
|
|
19
|
+
entries: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeVector(vector, dim) {
|
|
24
|
+
if (!vector || typeof vector.length !== "number") return null;
|
|
25
|
+
if (Number.isFinite(dim) && vector.length !== dim) return null;
|
|
26
|
+
const out = Array.from(vector, Number);
|
|
27
|
+
if (out.some((value) => !Number.isFinite(value))) return null;
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function dot(left, right) {
|
|
32
|
+
const length = Math.min(left.length, right.length);
|
|
33
|
+
let score = 0;
|
|
34
|
+
for (let i = 0; i < length; i += 1) score += left[i] * right[i];
|
|
35
|
+
return score;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateEmbedder(embedder) {
|
|
39
|
+
if (!embedder || typeof embedder !== "object") {
|
|
40
|
+
throw new Error(
|
|
41
|
+
'Mumpix semantic recall requires an embedder. Pass { embedder } or install optional package "mumpix-embed".'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (typeof embedder.id !== "string" || !embedder.id.trim()) {
|
|
45
|
+
throw new Error("Mumpix semantic recall embedder requires a stable string id.");
|
|
46
|
+
}
|
|
47
|
+
if (!Number.isInteger(embedder.dim) || embedder.dim <= 0) {
|
|
48
|
+
throw new Error("Mumpix semantic recall embedder requires a positive integer dim.");
|
|
49
|
+
}
|
|
50
|
+
if (typeof embedder.embed !== "function") {
|
|
51
|
+
throw new Error("Mumpix semantic recall embedder requires embed(texts) => Promise<vector[]>.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function loadDefaultEmbedder() {
|
|
56
|
+
try {
|
|
57
|
+
const mod = require("mumpix-embed");
|
|
58
|
+
const candidate =
|
|
59
|
+
typeof mod.createEmbedder === "function"
|
|
60
|
+
? mod.createEmbedder()
|
|
61
|
+
: mod.default || mod.embedder || mod;
|
|
62
|
+
return candidate;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const code = err && err.code;
|
|
65
|
+
if (code === "MODULE_NOT_FOUND") {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'Mumpix semantic recall requires optional package "mumpix-embed". Install it with: npm i mumpix-embed'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class SemanticSidecar {
|
|
75
|
+
constructor(dbPath, embedder, opts = {}) {
|
|
76
|
+
validateEmbedder(embedder);
|
|
77
|
+
this.dbPath = dbPath;
|
|
78
|
+
this.embedder = embedder;
|
|
79
|
+
this.vectorPath = opts.vectorPath || vectorPathForDb(dbPath);
|
|
80
|
+
this.vectorLogPath = opts.vectorLogPath || vectorLogPathForSnapshot(this.vectorPath);
|
|
81
|
+
this._needsPersist = false;
|
|
82
|
+
this._bySeq = new Map();
|
|
83
|
+
this.data = this._load();
|
|
84
|
+
if (this._needsPersist) this.persist();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_load() {
|
|
88
|
+
if (!fs.existsSync(this.vectorPath)) {
|
|
89
|
+
this._needsPersist = true;
|
|
90
|
+
const data = emptySidecar(this.embedder);
|
|
91
|
+
this._hydrateMap(data.entries);
|
|
92
|
+
this._loadJournal(data);
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const loaded = JSON.parse(fs.readFileSync(this.vectorPath, "utf8"));
|
|
97
|
+
if (
|
|
98
|
+
!loaded ||
|
|
99
|
+
loaded.embedder_id !== this.embedder.id ||
|
|
100
|
+
loaded.dim !== this.embedder.dim ||
|
|
101
|
+
!Array.isArray(loaded.entries)
|
|
102
|
+
) {
|
|
103
|
+
this._needsPersist = true;
|
|
104
|
+
this._discardJournal();
|
|
105
|
+
return emptySidecar(this.embedder);
|
|
106
|
+
}
|
|
107
|
+
const data = {
|
|
108
|
+
...emptySidecar(this.embedder),
|
|
109
|
+
entries: loaded.entries.filter(
|
|
110
|
+
(entry) =>
|
|
111
|
+
entry &&
|
|
112
|
+
Number.isInteger(entry.seq ?? entry.id) &&
|
|
113
|
+
typeof entry.content === "string" &&
|
|
114
|
+
Array.isArray(entry.vec) &&
|
|
115
|
+
entry.vec.length === this.embedder.dim
|
|
116
|
+
).map((entry) => this._normalizeEntry(entry)),
|
|
117
|
+
};
|
|
118
|
+
this._hydrateMap(data.entries);
|
|
119
|
+
this._loadJournal(data);
|
|
120
|
+
this._hydrateMap(data.entries);
|
|
121
|
+
return data;
|
|
122
|
+
} catch (_) {
|
|
123
|
+
this._discardJournal();
|
|
124
|
+
const data = emptySidecar(this.embedder);
|
|
125
|
+
this._hydrateMap(data.entries);
|
|
126
|
+
return data;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
persist() {
|
|
131
|
+
fs.mkdirSync(path.dirname(this.vectorPath), { recursive: true });
|
|
132
|
+
const tmp = `${this.vectorPath}.tmp.${process.pid}`;
|
|
133
|
+
fs.writeFileSync(tmp, JSON.stringify(this.data, null, 2), "utf8");
|
|
134
|
+
fs.renameSync(tmp, this.vectorPath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
compact() {
|
|
138
|
+
this.persist();
|
|
139
|
+
this._discardJournal();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
syncRecords(records) {
|
|
143
|
+
const bySeq = new Map(records.map((record) => [record.id, record]));
|
|
144
|
+
const before = this.data.entries.length;
|
|
145
|
+
this.data.entries = this.data.entries.filter((entry) => {
|
|
146
|
+
const record = bySeq.get(entry.seq);
|
|
147
|
+
return record && record.content === entry.content && record.ts === entry.ts;
|
|
148
|
+
});
|
|
149
|
+
this._hydrateMap(this.data.entries);
|
|
150
|
+
if (this.data.entries.length !== before) this.compact();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async reconcileRecords(records) {
|
|
154
|
+
const bySeq = new Map(records.map((record) => [record.id, record]));
|
|
155
|
+
const before = this.data.entries.length;
|
|
156
|
+
this.data.entries = this.data.entries.filter((entry) => {
|
|
157
|
+
const record = bySeq.get(entry.seq);
|
|
158
|
+
return record && record.content === entry.content && record.ts === entry.ts;
|
|
159
|
+
});
|
|
160
|
+
this._hydrateMap(this.data.entries);
|
|
161
|
+
|
|
162
|
+
const missing = records.filter((record) => !this._bySeq.has(record.id));
|
|
163
|
+
if (!missing.length) {
|
|
164
|
+
if (this.data.entries.length !== before) this.compact();
|
|
165
|
+
return { pruned: before - this.data.entries.length, embedded: 0 };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const vectors = await this.embedder.embed(missing.map((record) => record.content));
|
|
169
|
+
if (!Array.isArray(vectors) || vectors.length !== missing.length) {
|
|
170
|
+
throw new Error("Mumpix semantic recall embedder returned the wrong number of vectors.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < missing.length; i += 1) {
|
|
174
|
+
const record = missing[i];
|
|
175
|
+
const vec = normalizeVector(vectors[i], this.embedder.dim);
|
|
176
|
+
if (!vec) throw new Error("Mumpix semantic recall embedder returned an invalid vector.");
|
|
177
|
+
this._upsert({
|
|
178
|
+
seq: record.id,
|
|
179
|
+
record_id: record.id,
|
|
180
|
+
content: record.content,
|
|
181
|
+
ts: record.ts,
|
|
182
|
+
vec,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.compact();
|
|
187
|
+
return { pruned: before - (this.data.entries.length - missing.length), embedded: missing.length };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async addRecord(record) {
|
|
191
|
+
const [vector] = await this.embedder.embed([record.content]);
|
|
192
|
+
const vec = normalizeVector(vector, this.embedder.dim);
|
|
193
|
+
if (!vec) throw new Error("Mumpix semantic recall embedder returned an invalid vector.");
|
|
194
|
+
|
|
195
|
+
const entry = {
|
|
196
|
+
seq: record.id,
|
|
197
|
+
id: record.id,
|
|
198
|
+
record_id: record.id,
|
|
199
|
+
content: record.content,
|
|
200
|
+
ts: record.ts,
|
|
201
|
+
vec,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
this._upsert(entry);
|
|
205
|
+
this._appendJournal({ op: "upsert", entry });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
clear() {
|
|
209
|
+
this.data.entries = [];
|
|
210
|
+
this._bySeq.clear();
|
|
211
|
+
this.compact();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async query(text, k, floor) {
|
|
215
|
+
if (!this.data.entries.length) return [];
|
|
216
|
+
const [queryVector] = await this.embedder.embed([text]);
|
|
217
|
+
const qVec = normalizeVector(queryVector, this.embedder.dim);
|
|
218
|
+
if (!qVec) throw new Error("Mumpix semantic recall embedder returned an invalid query vector.");
|
|
219
|
+
|
|
220
|
+
return this.data.entries
|
|
221
|
+
.map((entry) => ({ ...entry, _score: dot(qVec, entry.vec) }))
|
|
222
|
+
.filter((entry) => entry._score >= floor)
|
|
223
|
+
.sort((left, right) => {
|
|
224
|
+
if (right._score !== left._score) return right._score - left._score;
|
|
225
|
+
if (right.ts !== left.ts) return right.ts - left.ts;
|
|
226
|
+
return left.id - right.id;
|
|
227
|
+
})
|
|
228
|
+
.slice(0, k);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_normalizeEntry(entry) {
|
|
232
|
+
const seq = entry.seq ?? entry.id;
|
|
233
|
+
return {
|
|
234
|
+
seq,
|
|
235
|
+
id: seq,
|
|
236
|
+
record_id: entry.record_id ?? seq,
|
|
237
|
+
content: entry.content,
|
|
238
|
+
ts: entry.ts,
|
|
239
|
+
vec: entry.vec,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_hydrateMap(entries) {
|
|
244
|
+
this._bySeq.clear();
|
|
245
|
+
for (const entry of entries) this._bySeq.set(entry.seq, entry);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_upsert(entry) {
|
|
249
|
+
const normalized = this._normalizeEntry(entry);
|
|
250
|
+
const existing = this._bySeq.get(normalized.seq);
|
|
251
|
+
if (existing) {
|
|
252
|
+
Object.assign(existing, normalized);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this._bySeq.set(normalized.seq, normalized);
|
|
256
|
+
this.data.entries.push(normalized);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_loadJournal(data) {
|
|
260
|
+
if (!fs.existsSync(this.vectorLogPath)) return;
|
|
261
|
+
const bySeq = new Map(data.entries.map((entry) => [entry.seq, entry]));
|
|
262
|
+
try {
|
|
263
|
+
const lines = fs.readFileSync(this.vectorLogPath, "utf8").split("\n");
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
if (!line.trim()) continue;
|
|
266
|
+
const event = JSON.parse(line);
|
|
267
|
+
if (event.op === "clear") {
|
|
268
|
+
data.entries = [];
|
|
269
|
+
bySeq.clear();
|
|
270
|
+
} else if (event.op === "upsert" && event.entry) {
|
|
271
|
+
const entry = this._normalizeEntry(event.entry);
|
|
272
|
+
if (
|
|
273
|
+
Number.isInteger(entry.seq) &&
|
|
274
|
+
typeof entry.content === "string" &&
|
|
275
|
+
Array.isArray(entry.vec) &&
|
|
276
|
+
entry.vec.length === this.embedder.dim
|
|
277
|
+
) {
|
|
278
|
+
const existing = bySeq.get(entry.seq);
|
|
279
|
+
if (existing) {
|
|
280
|
+
Object.assign(existing, entry);
|
|
281
|
+
} else {
|
|
282
|
+
bySeq.set(entry.seq, entry);
|
|
283
|
+
data.entries.push(entry);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} catch (_) {
|
|
289
|
+
this._discardJournal();
|
|
290
|
+
data.entries = [...bySeq.values()];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_appendJournal(event) {
|
|
295
|
+
fs.mkdirSync(path.dirname(this.vectorLogPath), { recursive: true });
|
|
296
|
+
fs.appendFileSync(this.vectorLogPath, `${JSON.stringify(event)}\n`, "utf8");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_discardJournal() {
|
|
300
|
+
try {
|
|
301
|
+
fs.unlinkSync(this.vectorLogPath);
|
|
302
|
+
} catch (_) {}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
SemanticSidecar,
|
|
308
|
+
loadDefaultEmbedder,
|
|
309
|
+
validateEmbedder,
|
|
310
|
+
vectorLogPathForSnapshot,
|
|
311
|
+
vectorPathForDb,
|
|
312
|
+
};
|