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.
Files changed (38) hide show
  1. package/CHANGELOG.md +82 -11
  2. package/README.md +278 -8
  3. package/bin/mumpix.js +1 -405
  4. package/examples/agent-memory.js +1 -1
  5. package/examples/basic.js +1 -1
  6. package/examples/behavioral-primitives.js +50 -0
  7. package/examples/recall-quality-suite.js +156 -0
  8. package/examples/verified-mode.js +1 -1
  9. package/package.json +24 -13
  10. package/scripts/bench-semantic.cjs +76 -0
  11. package/scripts/test-license-modes.cjs +87 -0
  12. package/scripts/test-phase0.cjs +137 -0
  13. package/scripts/test-semantic.cjs +411 -0
  14. package/src/brp/index.js +1 -0
  15. package/src/collapse/index.js +1 -0
  16. package/src/core/MumpixDB.js +264 -323
  17. package/src/core/audit.js +1 -173
  18. package/src/core/auth.js +1 -232
  19. package/src/core/inverted-index.js +249 -0
  20. package/src/core/license.js +1 -267
  21. package/src/core/ml-dsa.mjs +1 -25
  22. package/src/core/ml-kem.mjs +1 -32
  23. package/src/core/recall.js +226 -112
  24. package/src/core/semantic-sidecar.js +312 -0
  25. package/src/core/store.js +365 -288
  26. package/src/core/wal-writer.js +83 -0
  27. package/src/index.js +20 -34
  28. package/src/integrations/developer-sdk.js +1 -165
  29. package/src/integrations/langchain-official.js +1 -0
  30. package/src/integrations/langchain.js +1 -131
  31. package/src/integrations/llamaindex-official.js +1 -0
  32. package/src/integrations/llamaindex.js +1 -86
  33. package/src/integrations/vector-sidecar.js +325 -0
  34. package/src/rlp/index.js +1 -0
  35. package/src/temporal/engine.js +1 -1894
  36. package/src/temporal/indexes.js +1 -178
  37. package/src/temporal/operators.js +1 -186
  38. package/scripts/postinstall-auth.js +0 -101
@@ -1,329 +1,275 @@
1
- 'use strict';
2
-
3
- /**
4
- * MumpixDB main database class
5
- *
6
- * const { Mumpix } = require('mumpix')
7
- * const db = await Mumpix.open('./agent.mumpix', { consistency: 'strict' })
8
- *
9
- * await db.remember('User prefers TypeScript')
10
- * const ans = await db.recall('what language?')
11
- * const all = await db.list()
12
- * await db.clear()
13
- * await db.close()
14
- */
15
-
16
- const { MumpixStore } = require('./store');
17
- const { MumpixAudit } = require('./audit');
18
- const { recall, recallMany } = require('./recall');
19
- const { LicenseManager } = require('./license');
20
- const { getStoredLicenseKey } = require('./auth');
21
- const { answerTemporalQuery, materializeEvents, materializeEventIndex, extractEventsFromRecord } = require('../temporal/engine');
22
- const { appendEventIndex } = require('../temporal/indexes');
23
-
24
- const VALID_MODES = ['eventual', 'strict', 'verified'];
1
+ "use strict";
2
+
3
+ const { MumpixStore } = require("./store");
4
+ const { MumpixAudit } = require("./audit");
5
+ const { recall, recallMany: scoreRecallMany } = require("./recall");
6
+ const { LicenseManager } = require("./license");
7
+ const { getStoredLicenseKey } = require("./auth");
8
+ const { SemanticSidecar, loadDefaultEmbedder } = require("./semantic-sidecar");
9
+ const {
10
+ answerTemporalQuery,
11
+ materializeEvents,
12
+ materializeEventIndex,
13
+ extractEventsFromRecord,
14
+ } = require("../temporal/engine");
15
+ const { appendEventIndex } = require("../temporal/indexes");
16
+
17
+ const VALID_CONSISTENCY = ["eventual", "strict", "verified"];
18
+
19
+ function envFlag(name) {
20
+ const value = process.env[name];
21
+ return value != null && value !== "" && /^(1|true|yes|on)$/i.test(String(value).trim());
22
+ }
23
+
24
+ function telemetryEnabled(opts = {}) {
25
+ return typeof opts.telemetry === "boolean" ? opts.telemetry : envFlag("MUMPIX_TELEMETRY");
26
+ }
27
+
28
+ function storeOpenOptions(consistency, opts) {
29
+ return {
30
+ consistency,
31
+ lock: opts.lock,
32
+ lockStaleMs: opts.lockStaleMs,
33
+ walFlushIntervalMs: opts.walFlushIntervalMs,
34
+ walMaxPendingBytes: opts.walMaxPendingBytes,
35
+ recallIndex: opts.recallIndex,
36
+ };
37
+ }
38
+
39
+ function semanticOptions(opts = {}) {
40
+ const enabled = opts.recall === "semantic" || opts.semantic === true;
41
+ if (!enabled) return null;
42
+ const embedder = opts.embedder || loadDefaultEmbedder();
43
+ const semanticFloor = Number.isFinite(opts.semanticFloor) ? Number(opts.semanticFloor) : 0.5;
44
+ return {
45
+ embedder,
46
+ semanticFloor,
47
+ vectorPath: opts.vectorPath,
48
+ };
49
+ }
25
50
 
26
51
  class MumpixDB {
27
- /**
28
- * @param {MumpixStore} store
29
- * @param {MumpixAudit|null} audit
30
- * @param {LicenseManager} license
31
- * @param {object} opts
32
- */
33
52
  constructor(store, audit, license, opts = {}) {
34
53
  this._store = store;
35
- this._audit = audit; // non-null only in 'verified' mode
54
+ this._audit = audit;
36
55
  this._opts = opts;
37
56
  this._closed = false;
38
57
  this._license = license;
39
58
  this._temporalCache = null;
40
59
  }
41
60
 
42
- // ─────────────────────────────────────────
43
- // Factory
44
- // ─────────────────────────────────────────
45
-
46
- /**
47
- * Open (or create) a Mumpix database.
48
- *
49
- * @param {string} filePath Path to .mumpix file
50
- * @param {object} [opts]
51
- * @param {string} [opts.consistency] 'eventual' | 'strict' | 'verified' (default: 'eventual')
52
- * @param {Function} [opts.embedFn] async (texts: string[]) => number[][] — optional custom embeddings
53
- * @param {string} [opts.licenseKey] Commercial license key for Pro/Enterprise features
54
- * @returns {Promise<MumpixDB>}
55
- */
56
61
  static async open(filePath, opts = {}) {
57
- if (!filePath || typeof filePath !== 'string') {
58
- throw new TypeError('Mumpix.open() requires a file path string');
62
+ if (!filePath || typeof filePath !== "string") {
63
+ throw new TypeError("Mumpix.open() requires a file path string");
59
64
  }
60
65
 
61
- const requestedConsistency = opts.consistency || 'eventual';
62
- if (!VALID_MODES.includes(requestedConsistency)) {
63
- throw new Error(`Invalid consistency mode "${requestedConsistency}". Valid: ${VALID_MODES.join(', ')}`);
66
+ const requestedConsistency = opts.consistency || "eventual";
67
+ if (!VALID_CONSISTENCY.includes(requestedConsistency)) {
68
+ throw new Error(
69
+ `Invalid consistency mode "${requestedConsistency}". Valid: ${VALID_CONSISTENCY.join(", ")}`
70
+ );
64
71
  }
65
72
 
66
73
  const store = new MumpixStore(filePath);
67
- let auditLog = null;
74
+ let audit = null;
75
+
68
76
  try {
69
- await store.open({ consistency: requestedConsistency });
77
+ store.open(storeOpenOptions(requestedConsistency, opts));
70
78
 
71
- // License initialization & check.
72
- // Allow dependency injection for local verification and integration tests.
73
- const resolvedLicenseKey = opts.licenseKey || getStoredLicenseKey() || null;
74
- const license = new LicenseManager(resolvedLicenseKey);
79
+ const licenseKey = opts.licenseKey || getStoredLicenseKey() || null;
80
+ const license =
81
+ opts.licenseManager instanceof LicenseManager
82
+ ? opts.licenseManager
83
+ : new LicenseManager(licenseKey);
75
84
  license.setFileContext(store.header && store.header.fileId ? store.header.fileId : null);
76
- const licenseReady = await license.init();
77
- if (resolvedLicenseKey && !licenseReady) {
78
- console.warn(`[DEBUG] License rejected: ${license.validationError}`);
79
- }
85
+ await license.init();
80
86
 
81
- // Capability-gated mode check with graceful downgrade policy:
82
- // if a paid mode (strict/verified) is requested with expired/invalid license,
83
- // force eventual mode until the license is renewed.
84
- let effectiveConsistency = requestedConsistency;
85
- if (requestedConsistency !== 'eventual') {
87
+ let consistency = requestedConsistency;
88
+ if (requestedConsistency !== "eventual") {
86
89
  try {
87
- license.checkLimit('mode', requestedConsistency);
90
+ license.checkLimit("mode", requestedConsistency);
88
91
  license.assertActive(requestedConsistency);
89
- } catch (modeErr) {
90
- const msg = String(modeErr && modeErr.message ? modeErr.message : modeErr).toLowerCase();
91
- console.warn(`[DEBUG] Mode check failed: ${msg}`);
92
- const downgradeable =
93
- msg.includes('expired') ||
94
- msg.includes('invalid for mode') ||
95
- msg.includes('license_validation_failed') ||
96
- msg.includes('quantum signature verification failed') ||
97
- msg.includes('lease window') ||
98
- msg.includes('too far in the future');
99
- if (!downgradeable) throw modeErr;
100
- effectiveConsistency = 'eventual';
92
+ } catch (err) {
93
+ const message = String((err && err.message) || err).toLowerCase();
94
+ const canDowngrade =
95
+ message.includes("expired") ||
96
+ message.includes("invalid for mode") ||
97
+ message.includes("license_validation_failed") ||
98
+ message.includes("quantum signature verification failed") ||
99
+ message.includes("lease window") ||
100
+ message.includes("too far in the future");
101
+ if (!canDowngrade) throw err;
102
+ consistency = "eventual";
101
103
  }
102
104
  }
103
105
 
104
- if (effectiveConsistency !== requestedConsistency) {
105
- console.warn(`[DEBUG] Downgrading consistency to ${effectiveConsistency}`);
106
- // Re-open with downgraded mode after releasing current FD/lock first.
106
+ if (consistency !== requestedConsistency) {
107
107
  store.close();
108
- await store.open({ consistency: effectiveConsistency });
108
+ store.open(storeOpenOptions(consistency, opts));
109
109
  }
110
110
 
111
- if (effectiveConsistency === 'verified') {
112
- auditLog = new MumpixAudit(filePath);
113
- auditLog.open();
111
+ if (consistency === "verified") {
112
+ audit = new MumpixAudit(filePath);
113
+ audit.open();
114
114
  }
115
115
 
116
- return new MumpixDB(store, auditLog, license, {
116
+ const finalOpts = {
117
117
  ...opts,
118
- consistency: effectiveConsistency,
118
+ telemetry: telemetryEnabled(opts),
119
+ consistency,
119
120
  requestedConsistency,
120
- downgradedConsistency: effectiveConsistency !== requestedConsistency
121
- ? { from: requestedConsistency, to: effectiveConsistency, reason: 'license_not_active' }
122
- : null,
123
- });
124
- } catch (error) {
125
- if (auditLog) {
126
- try { auditLog.close(); } catch (_) {}
121
+ downgradedConsistency:
122
+ consistency !== requestedConsistency
123
+ ? { from: requestedConsistency, to: consistency, reason: "license_not_active" }
124
+ : null,
125
+ };
126
+
127
+ const semantic = semanticOptions(finalOpts);
128
+ if (semantic) {
129
+ finalOpts.semanticRecall = new SemanticSidecar(store.filePath, semantic.embedder, {
130
+ vectorPath: semantic.vectorPath,
131
+ });
132
+ finalOpts.semanticFloor = semantic.semanticFloor;
133
+ await finalOpts.semanticRecall.reconcileRecords(store.all());
127
134
  }
128
- try { store.close(); } catch (_) {}
129
- throw error;
135
+
136
+ return new MumpixDB(store, audit, license, finalOpts);
137
+ } catch (err) {
138
+ if (audit) {
139
+ try {
140
+ audit.close();
141
+ } catch (_) {}
142
+ }
143
+ try {
144
+ store.close();
145
+ } catch (_) {}
146
+ throw err;
130
147
  }
131
148
  }
132
149
 
133
- // ─────────────────────────────────────────
134
- // Core API
135
- // ─────────────────────────────────────────
136
-
137
- /**
138
- * Store a memory.
139
- *
140
- * @param {string} content
141
- * @returns {{ id: number, written: boolean, consistency: string }}
142
- */
143
- async remember(content, opts = {}) {
150
+ async remember(input, meta = {}) {
144
151
  this._assertOpen();
145
- let text = content;
146
- let meta = opts;
147
152
 
148
- if (content && typeof content === 'object' && !Array.isArray(content)) {
149
- text = content.content;
150
- meta = {
151
- ...opts,
152
- workspace: content.workspace ?? opts.workspace,
153
- repo: content.repo ?? opts.repo,
154
- source: content.source ?? opts.source,
155
- ts: content.ts ?? opts.ts,
153
+ let content = input;
154
+ let cleanMeta = meta;
155
+ if (input && typeof input === "object" && !Array.isArray(input)) {
156
+ content = input.content;
157
+ cleanMeta = {
158
+ ...meta,
159
+ workspace: input.workspace ?? meta.workspace,
160
+ repo: input.repo ?? meta.repo,
161
+ source: input.source ?? meta.source,
162
+ ts: input.ts ?? meta.ts,
156
163
  };
157
164
  }
158
165
 
159
- if (typeof text !== 'string' || !text.trim()) {
160
- throw new TypeError('remember() requires a non-empty string or { content } object');
166
+ if (typeof content !== "string" || !content.trim()) {
167
+ throw new TypeError("remember() requires a non-empty string or { content } object");
161
168
  }
162
169
 
163
- // License Check for Record Limit
164
- this._license.checkLimit('records', this._store.records.length);
165
-
166
- const record = this._store.write(text, meta);
170
+ this._license.checkLimit("records", this._store.records.length);
171
+ const record = this._store.write(content, cleanMeta);
167
172
  this._appendTemporalRecord(record);
173
+ await this._appendSemanticRecord(record);
174
+ if (this._audit) this._audit.logWrite(record);
175
+ return { written: true, id: record.id, consistency: this._store.header.consistency };
176
+ }
168
177
 
169
- if (this._audit) {
170
- this._audit.logWrite(record);
171
- }
178
+ async rememberAll(items) {
179
+ this._assertOpen();
180
+ if (!Array.isArray(items)) throw new TypeError("rememberAll() expects an array");
181
+ this._license.checkLimit("records", this._store.records.length + items.length);
172
182
 
173
- return {
183
+ const normalized = items.map((item) => {
184
+ if (item && typeof item === "object" && !Array.isArray(item)) {
185
+ return item;
186
+ }
187
+ return { content: item };
188
+ });
189
+
190
+ const records = this._store.writeBatch(normalized);
191
+ for (const record of records) {
192
+ this._appendTemporalRecord(record);
193
+ await this._appendSemanticRecord(record);
194
+ if (this._audit) this._audit.logWrite(record);
195
+ }
196
+ return records.map((record) => ({
174
197
  written: true,
175
198
  id: record.id,
176
199
  consistency: this._store.header.consistency,
177
- };
200
+ }));
178
201
  }
179
202
 
180
- /**
181
- * Recall the most semantically relevant memory for a query.
182
- *
183
- * @param {string} query
184
- * @param {object} [opts]
185
- * @param {object} [opts.filter] Pre-filter records: fn(record) → bool
186
- * @param {number} [opts.since] Only consider records newer than this timestamp
187
- * @param {string} [opts.mode] 'hybrid' | 'exact' | 'semantic'
188
- * @returns {string|null}
189
- */
190
203
  async recall(query, opts = {}) {
191
204
  this._assertOpen();
192
- if (typeof query !== 'string' || !query.trim()) {
193
- throw new TypeError('recall() requires a non-empty string');
194
- }
195
-
196
- const records = this._store.all();
197
- if (!records.length) return null;
198
-
199
- const t0 = Date.now();
200
- const result = await recall(query, records, {
201
- embedFn: this._opts.embedFn,
202
- ...opts,
203
- });
204
- const latencyMs = Date.now() - t0;
205
-
206
- if (this._audit) {
207
- this._audit.logRecall(query, result, latencyMs);
205
+ if (typeof query !== "string" || !query.trim()) {
206
+ throw new TypeError("recall() requires a non-empty string");
208
207
  }
209
208
 
210
- return result ? result.content : null;
209
+ const start = Date.now();
210
+ const hits = await this._rank(query, 1, opts);
211
+ const hit = hits[0] || null;
212
+ const latency = Date.now() - start;
213
+ if (this._audit) this._audit.logRecall(query, hit, latency);
214
+ return hit ? hit.content : null;
211
215
  }
212
216
 
213
- /**
214
- * Return the top-k most relevant memories.
215
- *
216
- * @param {string} query
217
- * @param {number} [k=5]
218
- * @param {object} [opts] Same opts as recall()
219
- * @returns {Array<{ id, content, ts, score }>}
220
- */
221
217
  async recallMany(query, k = 5, opts = {}) {
222
218
  this._assertOpen();
223
- if (typeof query !== 'string' || !query.trim()) {
224
- throw new TypeError('recallMany() requires a non-empty string');
219
+ if (typeof query !== "string" || !query.trim()) {
220
+ throw new TypeError("recallMany() requires a non-empty string");
225
221
  }
226
222
 
227
- const records = this._store.all();
228
- if (!records.length) return [];
229
-
230
- const results = await recallMany(query, records, {
231
- embedFn: this._opts.embedFn,
232
- k,
233
- ...opts,
234
- });
235
-
236
- return results.map(r => ({
237
- id: r.id,
238
- content: r.content,
239
- ts: r.ts,
240
- score: r._score,
223
+ return (await this._rank(query, k, opts)).map((record) => ({
224
+ id: record.id,
225
+ content: record.content,
226
+ ts: record.ts,
227
+ score: record._score,
241
228
  }));
242
229
  }
243
230
 
244
- /**
245
- * List all stored memories.
246
- *
247
- * @returns {Array<{ id, content, ts }>}
248
- */
249
231
  async list() {
250
232
  this._assertOpen();
251
- return this._store.all().map(r => ({
252
- id: r.id,
253
- content: r.content,
254
- ts: r.ts,
233
+ return this._store.all().map((record) => ({
234
+ id: record.id,
235
+ content: record.content,
236
+ ts: record.ts,
255
237
  }));
256
238
  }
257
239
 
258
- /**
259
- * Materialize normalized temporal events from WAL-backed records.
260
- *
261
- * @param {object} [opts]
262
- * @param {string} [opts.query] Optional query to shortlist candidate records first
263
- * @param {number} [opts.topK=50]
264
- * @returns {Array<object>}
265
- */
266
240
  async events(opts = {}) {
267
241
  this._assertOpen();
268
242
  const records = this._store.all();
269
243
  if (!records.length) return [];
270
-
271
- if (!opts.query) {
272
- return this._getTemporalCache().events;
273
- }
274
-
275
- if (opts.query && typeof opts.query === 'string' && opts.query.trim()) {
244
+ if (!opts.query) return this._getTemporalCache().events;
245
+ if (opts.query && typeof opts.query === "string" && opts.query.trim()) {
276
246
  const topK = Number.isFinite(opts.topK) ? Number(opts.topK) : 50;
277
- const shortlisted = await recallMany(opts.query, records, {
247
+ const hits = await scoreRecallMany(opts.query, records, {
278
248
  embedFn: this._opts.embedFn,
279
249
  k: topK,
280
250
  });
281
- return materializeEvents(shortlisted);
251
+ return materializeEvents(hits);
282
252
  }
283
-
284
253
  return this._getTemporalCache().events;
285
254
  }
286
255
 
287
- /**
288
- * Return derived event indexes built from WAL-backed records.
289
- *
290
- * @param {object} [opts]
291
- * @param {string} [opts.query]
292
- * @param {number} [opts.topK=50]
293
- * @returns {object}
294
- */
295
256
  async eventIndex(opts = {}) {
296
257
  this._assertOpen();
297
258
  const records = this._store.all();
298
- if (!records.length) {
299
- return materializeEventIndex([]);
300
- }
301
-
302
- if (!opts.query) {
303
- return this._getTemporalCache();
304
- }
305
-
259
+ if (!records.length) return materializeEventIndex([]);
260
+ if (!opts.query) return this._getTemporalCache();
306
261
  const topK = Number.isFinite(opts.topK) ? Number(opts.topK) : 50;
307
- const shortlisted = await recallMany(opts.query, records, {
262
+ const hits = await scoreRecallMany(opts.query, records, {
308
263
  embedFn: this._opts.embedFn,
309
264
  k: topK,
310
265
  });
311
- return materializeEventIndex(shortlisted);
266
+ return materializeEventIndex(hits);
312
267
  }
313
268
 
314
- /**
315
- * Run a deterministic temporal query over WAL-backed records.
316
- *
317
- * @param {string} query
318
- * @param {object} [opts]
319
- * @param {number} [opts.topK=50]
320
- * @param {Date|string|number} [opts.now]
321
- * @returns {object}
322
- */
323
269
  async temporalQuery(query, opts = {}) {
324
270
  this._assertOpen();
325
- if (typeof query !== 'string' || !query.trim()) {
326
- throw new TypeError('temporalQuery() requires a non-empty string');
271
+ if (typeof query !== "string" || !query.trim()) {
272
+ throw new TypeError("temporalQuery() requires a non-empty string");
327
273
  }
328
274
 
329
275
  const records = this._store.all();
@@ -333,157 +279,148 @@ class MumpixDB {
333
279
  kind: null,
334
280
  value: null,
335
281
  evidence: [],
336
- warnings: ['No records available.'],
282
+ warnings: ["No records available."],
337
283
  parsed_query: null,
338
284
  events: [],
339
285
  };
340
286
  }
341
287
 
342
288
  const topK = Number.isFinite(opts.topK) ? Number(opts.topK) : 50;
343
- const shortlisted = await recallMany(query, records, {
289
+ const hits = await scoreRecallMany(query, records, {
344
290
  embedFn: this._opts.embedFn,
345
291
  k: topK,
346
292
  });
347
-
348
293
  const now =
349
- opts.now instanceof Date ? opts.now :
350
- (typeof opts.now === 'string' || typeof opts.now === 'number') ? new Date(opts.now) :
351
- new Date();
352
-
353
- return answerTemporalQuery(query, shortlisted, { now });
294
+ opts.now instanceof Date
295
+ ? opts.now
296
+ : typeof opts.now === "string" || typeof opts.now === "number"
297
+ ? new Date(opts.now)
298
+ : new Date();
299
+ return answerTemporalQuery(query, hits, { now });
354
300
  }
355
301
 
356
- /**
357
- * Delete all memories.
358
- *
359
- * @returns {{ cleared: boolean, count: number }}
360
- */
361
302
  async clear() {
362
303
  this._assertOpen();
363
304
  const count = this._store.clear();
364
305
  this._invalidateTemporalCache();
365
-
366
- if (this._audit) {
367
- this._audit.logClear(count);
368
- }
369
-
306
+ if (this._opts.semanticRecall) this._opts.semanticRecall.clear();
307
+ if (this._audit) this._audit.logClear(count);
370
308
  return { cleared: true, count };
371
309
  }
372
310
 
373
- /**
374
- * Get audit log entries (Verified mode only).
375
- *
376
- * @returns {Array<object>}
377
- */
378
311
  async audit() {
379
312
  this._assertOpen();
380
313
  if (!this._audit) {
381
- throw new Error('audit() requires consistency: "verified". Current mode: ' + this._store.header.consistency);
314
+ throw new Error(
315
+ `audit() requires consistency: "verified". Current mode: ${this._store.header.consistency}`
316
+ );
382
317
  }
383
318
  return this._audit.all();
384
319
  }
385
320
 
386
- /**
387
- * Get audit log summary (Verified mode only).
388
- */
389
321
  async auditSummary() {
390
322
  this._assertOpen();
391
- if (!this._audit) {
392
- throw new Error('auditSummary() requires consistency: "verified"');
393
- }
323
+ if (!this._audit) throw new Error('auditSummary() requires consistency: "verified"');
394
324
  return this._audit.summary();
395
325
  }
396
326
 
397
- /**
398
- * Export audit log as NDJSON string (Verified mode only).
399
- */
400
327
  async exportAudit() {
401
328
  this._assertOpen();
402
- if (!this._audit) {
403
- throw new Error('exportAudit() requires consistency: "verified"');
404
- }
329
+ if (!this._audit) throw new Error('exportAudit() requires consistency: "verified"');
405
330
  return this._audit.export();
406
331
  }
407
332
 
408
- /**
409
- * Database metadata and stats.
410
- */
411
333
  async stats() {
412
334
  this._assertOpen();
413
335
  return this._store.stats();
414
336
  }
415
337
 
416
- /**
417
- * Close the database and release file handles.
418
- */
419
- async close() {
420
- if (this._closed) return;
421
-
422
- // Sync usage metrics in background on close
423
- this._license.syncUsage(this._store.stats()).catch(() => { });
424
-
425
- this._store.close();
426
- if (this._audit) this._audit.close();
427
- this._closed = true;
338
+ async flush() {
339
+ this._assertOpen();
340
+ this._store.flush();
341
+ if (this._opts.semanticRecall) this._opts.semanticRecall.compact();
342
+ return { flushed: true };
428
343
  }
429
344
 
430
- // ─────────────────────────────────────────
431
- // Convenience / fluent helpers
432
- // ─────────────────────────────────────────
345
+ async rebuildIndex() {
346
+ this._assertOpen();
347
+ return this._store.rebuildIndex();
348
+ }
433
349
 
434
- /**
435
- * Add multiple memories at once.
436
- *
437
- * @param {string[]} items
438
- */
439
- async rememberAll(items) {
350
+ async indexStats() {
440
351
  this._assertOpen();
441
- if (!Array.isArray(items)) throw new TypeError('rememberAll() expects an array');
442
- const results = [];
443
- for (const item of items) {
444
- results.push(await this.remember(item));
352
+ return this._store.indexStats();
353
+ }
354
+
355
+ async close() {
356
+ if (this._closed) return;
357
+ try {
358
+ if (this._opts.telemetry && this._license.hasVerifiedLicense()) {
359
+ await this._license.syncUsage(this._store.stats());
360
+ }
361
+ } finally {
362
+ if (this._opts.semanticRecall) {
363
+ try {
364
+ this._opts.semanticRecall.compact();
365
+ } catch (_) {}
366
+ }
367
+ this._store.close();
368
+ if (this._audit) this._audit.close();
369
+ this._closed = true;
445
370
  }
446
- return results;
447
371
  }
448
372
 
449
- /**
450
- * Check if any memory semantically matches a query.
451
- *
452
- * @param {string} query
453
- * @returns {boolean}
454
- */
455
373
  async has(query) {
456
- const result = await this.recall(query);
457
- return result !== null;
374
+ return (await this.recall(query)) !== null;
458
375
  }
459
376
 
460
- /**
461
- * Return the number of stored memories.
462
- */
463
377
  get size() {
464
378
  return this._store.records.length;
465
379
  }
466
380
 
467
- /**
468
- * Consistency mode of this instance.
469
- */
470
381
  get consistency() {
471
382
  return this._store.header.consistency;
472
383
  }
473
384
 
474
- /**
475
- * File path of the underlying .mumpix file.
476
- */
477
385
  get path() {
478
386
  return this._store.filePath;
479
387
  }
480
388
 
481
- // ─────────────────────────────────────────
482
- // Private
483
- // ─────────────────────────────────────────
389
+ async _rank(query, k, opts = {}) {
390
+ const allRecords = this._store.all();
391
+ if (!allRecords.length) return [];
392
+
393
+ let candidates = null;
394
+ if (opts.recallIndex !== false && typeof this._store.indexedCandidates === "function") {
395
+ candidates = this._store.indexedCandidates(query, opts.candidateCap || 400);
396
+ }
397
+
398
+ const records = candidates && candidates.length ? candidates : allRecords;
399
+ const ranked = await scoreRecallMany(query, records, {
400
+ embedFn: this._opts.embedFn,
401
+ k,
402
+ ...opts,
403
+ });
404
+ const verified = ranked.filter((record) => this._store._verifyHash(record));
405
+ if (verified.length || !this._opts.semanticRecall || opts.semantic === false) return verified;
406
+
407
+ const semanticHits = await this._opts.semanticRecall.query(
408
+ query,
409
+ k,
410
+ Number.isFinite(opts.semanticFloor) ? Number(opts.semanticFloor) : this._opts.semanticFloor
411
+ );
412
+ return semanticHits.map((hit) => ({
413
+ id: hit.id,
414
+ content: hit.content,
415
+ ts: hit.ts,
416
+ h: this._store.records.find((record) => record.id === hit.id)?.h,
417
+ _score: hit._score,
418
+ _via: "semantic",
419
+ }));
420
+ }
484
421
 
485
422
  _assertOpen() {
486
- if (this._closed) throw new Error('Database is closed. Call Mumpix.open() again.');
423
+ if (this._closed) throw new Error("Database is closed. Call Mumpix.open() again.");
487
424
  this._license.assertActive(this._store.header.consistency);
488
425
  }
489
426
 
@@ -493,8 +430,8 @@ class MumpixDB {
493
430
 
494
431
  _temporalSignature() {
495
432
  const records = this._store.records || [];
496
- const last = records.length ? records[records.length - 1] : null;
497
- return `${records.length}:${last ? last.id : 0}:${last ? last.ts : 0}`;
433
+ const tail = records.length ? records[records.length - 1] : null;
434
+ return `${records.length}:${tail ? tail.id : 0}:${tail ? tail.ts : 0}`;
498
435
  }
499
436
 
500
437
  _getTemporalCache() {
@@ -502,7 +439,6 @@ class MumpixDB {
502
439
  if (this._temporalCache && this._temporalCache.signature === signature) {
503
440
  return this._temporalCache.index;
504
441
  }
505
-
506
442
  const index = materializeEventIndex(this._store.all());
507
443
  this._temporalCache = { signature, index };
508
444
  return index;
@@ -514,6 +450,11 @@ class MumpixDB {
514
450
  appendEventIndex(this._temporalCache.index, events);
515
451
  this._temporalCache.signature = this._temporalSignature();
516
452
  }
453
+
454
+ async _appendSemanticRecord(record) {
455
+ if (!this._opts.semanticRecall) return;
456
+ await this._opts.semanticRecall.addRecord(record);
457
+ }
517
458
  }
518
459
 
519
460
  module.exports = { MumpixDB };