mumpix 1.0.0
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.
Potentially problematic release.
This version of mumpix might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/bin/mumpix.js +305 -0
- package/examples/agent-memory.js +63 -0
- package/examples/basic.js +44 -0
- package/examples/verified-mode.js +85 -0
- package/package.json +50 -0
- package/src/core/MumpixDB.js +306 -0
- package/src/core/audit.js +173 -0
- package/src/core/recall.js +176 -0
- package/src/core/store.js +230 -0
- package/src/index.js +38 -0
- package/src/integrations/langchain.js +131 -0
- package/src/integrations/llamaindex.js +86 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* examples/verified-mode.js
|
|
5
|
+
*
|
|
6
|
+
* Demonstrates Mumpix's "Verified" consistency mode:
|
|
7
|
+
* - Immutable audit log (.mumpix.audit)
|
|
8
|
+
* - Every write and recall is logged with hashes
|
|
9
|
+
* - Reproducible recall behavior
|
|
10
|
+
* - Export audit log as NDJSON for compliance hand-off
|
|
11
|
+
*
|
|
12
|
+
* Run:
|
|
13
|
+
* node examples/verified-mode.js
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { Mumpix } = require('../src/index');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
|
|
21
|
+
const DB_PATH = path.join(os.tmpdir(), 'mumpix-verified-demo.mumpix');
|
|
22
|
+
|
|
23
|
+
;(async () => {
|
|
24
|
+
// Clean up any previous run
|
|
25
|
+
[DB_PATH, DB_PATH + '.audit', DB_PATH + '.wal'].forEach(f => {
|
|
26
|
+
if (fs.existsSync(f)) fs.unlinkSync(f);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
console.log('Opening Mumpix in Verified mode...\n');
|
|
30
|
+
const db = Mumpix.open(DB_PATH, { consistency: 'verified' });
|
|
31
|
+
|
|
32
|
+
// Simulate a compliance-relevant workflow
|
|
33
|
+
await db.remember('Loan application #4821 approved — credit score: 742');
|
|
34
|
+
await db.remember('Manual override applied by risk-officer-07 on 2025-02-18');
|
|
35
|
+
await db.remember('Model version: risk-v3.2.1, threshold: 0.72');
|
|
36
|
+
await db.remember('Basel III compliance flag: ACTIVE');
|
|
37
|
+
await db.remember('Customer consent recorded: 2025-02-17T14:32:00Z');
|
|
38
|
+
|
|
39
|
+
console.log(`Stored ${db.size} records.\n`);
|
|
40
|
+
|
|
41
|
+
// Recall queries
|
|
42
|
+
const queries = [
|
|
43
|
+
'Was there a manual override?',
|
|
44
|
+
'What model was used?',
|
|
45
|
+
'Is it Basel III compliant?',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
console.log('Recall queries:');
|
|
49
|
+
for (const q of queries) {
|
|
50
|
+
const ans = await db.recall(q);
|
|
51
|
+
console.log(` Q: ${q}`);
|
|
52
|
+
console.log(` A: ${ans ?? '(no match)'}`);
|
|
53
|
+
console.log();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Audit log
|
|
57
|
+
const entries = await db.audit();
|
|
58
|
+
console.log(`Audit log (${entries.length} entries):`);
|
|
59
|
+
entries.forEach(e => {
|
|
60
|
+
const ts = new Date(e.ts).toISOString();
|
|
61
|
+
const type = e._type.padEnd(10);
|
|
62
|
+
const detail =
|
|
63
|
+
e._type === 'write' ? `id=${e.id} hash=${e.hash} len=${e.len}`
|
|
64
|
+
: e._type === 'recall' ? `queryHash=${e.queryHash} resultId=${e.resultId ?? 'null'} ${e.latencyMs}ms`
|
|
65
|
+
: '';
|
|
66
|
+
console.log(` [${ts}] ${type} ${detail}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Audit summary
|
|
70
|
+
const summary = await db.auditSummary();
|
|
71
|
+
console.log('\nAudit summary:', JSON.stringify(summary, null, 2));
|
|
72
|
+
|
|
73
|
+
// Export
|
|
74
|
+
const ndjson = await db.exportAudit();
|
|
75
|
+
const exportPath = DB_PATH.replace('.mumpix', '-audit-export.ndjson');
|
|
76
|
+
fs.writeFileSync(exportPath, ndjson, 'utf8');
|
|
77
|
+
console.log(`\nAudit exported to: ${exportPath}`);
|
|
78
|
+
|
|
79
|
+
await db.close();
|
|
80
|
+
|
|
81
|
+
// Cleanup
|
|
82
|
+
[DB_PATH, DB_PATH + '.audit', exportPath].forEach(f => {
|
|
83
|
+
if (fs.existsSync(f)) fs.unlinkSync(f);
|
|
84
|
+
});
|
|
85
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mumpix",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SQLite for AI — embedded, zero-config memory database for AI agents and LLM applications",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mumpix": "./bin/mumpix.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"memory",
|
|
15
|
+
"database",
|
|
16
|
+
"embedded",
|
|
17
|
+
"vector",
|
|
18
|
+
"semantic",
|
|
19
|
+
"llm",
|
|
20
|
+
"agent",
|
|
21
|
+
"langchain",
|
|
22
|
+
"local-first",
|
|
23
|
+
"sqlite",
|
|
24
|
+
"storage",
|
|
25
|
+
"retrieval",
|
|
26
|
+
"rag"
|
|
27
|
+
],
|
|
28
|
+
"author": "Mumpix",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {},
|
|
34
|
+
"devDependencies": {},
|
|
35
|
+
"files": [
|
|
36
|
+
"src/",
|
|
37
|
+
"bin/",
|
|
38
|
+
"examples/",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/mumpix/mumpix.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/mumpix/mumpix/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://mumpix.dev"
|
|
50
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
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
|
+
|
|
20
|
+
const VALID_MODES = ['eventual', 'strict', 'verified'];
|
|
21
|
+
|
|
22
|
+
class MumpixDB {
|
|
23
|
+
/**
|
|
24
|
+
* @param {MumpixStore} store
|
|
25
|
+
* @param {MumpixAudit|null} audit
|
|
26
|
+
* @param {object} opts
|
|
27
|
+
*/
|
|
28
|
+
constructor(store, audit, opts = {}) {
|
|
29
|
+
this._store = store;
|
|
30
|
+
this._audit = audit; // non-null only in 'verified' mode
|
|
31
|
+
this._opts = opts;
|
|
32
|
+
this._closed = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────
|
|
36
|
+
// Factory
|
|
37
|
+
// ─────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Open (or create) a Mumpix database.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} filePath Path to .mumpix file
|
|
43
|
+
* @param {object} [opts]
|
|
44
|
+
* @param {string} [opts.consistency] 'eventual' | 'strict' | 'verified' (default: 'eventual')
|
|
45
|
+
* @param {Function} [opts.embedFn] async (texts: string[]) => number[][] — optional custom embeddings
|
|
46
|
+
* @returns {MumpixDB}
|
|
47
|
+
*/
|
|
48
|
+
static open(filePath, opts = {}) {
|
|
49
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
50
|
+
throw new TypeError('Mumpix.open() requires a file path string');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const consistency = opts.consistency || 'eventual';
|
|
54
|
+
if (!VALID_MODES.includes(consistency)) {
|
|
55
|
+
throw new Error(`Invalid consistency mode "${consistency}". Valid: ${VALID_MODES.join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const store = new MumpixStore(filePath);
|
|
59
|
+
store.open({ consistency });
|
|
60
|
+
|
|
61
|
+
let auditLog = null;
|
|
62
|
+
if (consistency === 'verified') {
|
|
63
|
+
auditLog = new MumpixAudit(filePath);
|
|
64
|
+
auditLog.open();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return new MumpixDB(store, auditLog, { ...opts, consistency });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─────────────────────────────────────────
|
|
71
|
+
// Core API
|
|
72
|
+
// ─────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Store a memory.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} content
|
|
78
|
+
* @returns {{ id: number, written: boolean, consistency: string }}
|
|
79
|
+
*/
|
|
80
|
+
async remember(content) {
|
|
81
|
+
this._assertOpen();
|
|
82
|
+
if (typeof content !== 'string' || !content.trim()) {
|
|
83
|
+
throw new TypeError('remember() requires a non-empty string');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const record = this._store.write(content);
|
|
87
|
+
|
|
88
|
+
if (this._audit) {
|
|
89
|
+
this._audit.logWrite(record);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
written: true,
|
|
94
|
+
id: record.id,
|
|
95
|
+
consistency: this._store.header.consistency,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Recall the most semantically relevant memory for a query.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} query
|
|
103
|
+
* @param {object} [opts]
|
|
104
|
+
* @param {object} [opts.filter] Pre-filter records: fn(record) → bool
|
|
105
|
+
* @param {number} [opts.since] Only consider records newer than this timestamp
|
|
106
|
+
* @param {string} [opts.mode] 'hybrid' | 'exact' | 'semantic'
|
|
107
|
+
* @returns {string|null}
|
|
108
|
+
*/
|
|
109
|
+
async recall(query, opts = {}) {
|
|
110
|
+
this._assertOpen();
|
|
111
|
+
if (typeof query !== 'string' || !query.trim()) {
|
|
112
|
+
throw new TypeError('recall() requires a non-empty string');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const records = this._store.all();
|
|
116
|
+
if (!records.length) return null;
|
|
117
|
+
|
|
118
|
+
const t0 = Date.now();
|
|
119
|
+
const result = await recall(query, records, {
|
|
120
|
+
embedFn: this._opts.embedFn,
|
|
121
|
+
...opts,
|
|
122
|
+
});
|
|
123
|
+
const latencyMs = Date.now() - t0;
|
|
124
|
+
|
|
125
|
+
if (this._audit) {
|
|
126
|
+
this._audit.logRecall(query, result, latencyMs);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result ? result.content : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Return the top-k most relevant memories.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} query
|
|
136
|
+
* @param {number} [k=5]
|
|
137
|
+
* @param {object} [opts] Same opts as recall()
|
|
138
|
+
* @returns {Array<{ id, content, ts, score }>}
|
|
139
|
+
*/
|
|
140
|
+
async recallMany(query, k = 5, opts = {}) {
|
|
141
|
+
this._assertOpen();
|
|
142
|
+
if (typeof query !== 'string' || !query.trim()) {
|
|
143
|
+
throw new TypeError('recallMany() requires a non-empty string');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const records = this._store.all();
|
|
147
|
+
if (!records.length) return [];
|
|
148
|
+
|
|
149
|
+
const results = await recallMany(query, records, {
|
|
150
|
+
embedFn: this._opts.embedFn,
|
|
151
|
+
k,
|
|
152
|
+
...opts,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return results.map(r => ({
|
|
156
|
+
id: r.id,
|
|
157
|
+
content: r.content,
|
|
158
|
+
ts: r.ts,
|
|
159
|
+
score: r._score,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* List all stored memories.
|
|
165
|
+
*
|
|
166
|
+
* @returns {Array<{ id, content, ts }>}
|
|
167
|
+
*/
|
|
168
|
+
async list() {
|
|
169
|
+
this._assertOpen();
|
|
170
|
+
return this._store.all().map(r => ({
|
|
171
|
+
id: r.id,
|
|
172
|
+
content: r.content,
|
|
173
|
+
ts: r.ts,
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Delete all memories.
|
|
179
|
+
*
|
|
180
|
+
* @returns {{ cleared: boolean, count: number }}
|
|
181
|
+
*/
|
|
182
|
+
async clear() {
|
|
183
|
+
this._assertOpen();
|
|
184
|
+
const count = this._store.clear();
|
|
185
|
+
|
|
186
|
+
if (this._audit) {
|
|
187
|
+
this._audit.logClear(count);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { cleared: true, count };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get audit log entries (Verified mode only).
|
|
195
|
+
*
|
|
196
|
+
* @returns {Array<object>}
|
|
197
|
+
*/
|
|
198
|
+
async audit() {
|
|
199
|
+
this._assertOpen();
|
|
200
|
+
if (!this._audit) {
|
|
201
|
+
throw new Error('audit() requires consistency: "verified". Current mode: ' + this._store.header.consistency);
|
|
202
|
+
}
|
|
203
|
+
return this._audit.all();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get audit log summary (Verified mode only).
|
|
208
|
+
*/
|
|
209
|
+
async auditSummary() {
|
|
210
|
+
this._assertOpen();
|
|
211
|
+
if (!this._audit) {
|
|
212
|
+
throw new Error('auditSummary() requires consistency: "verified"');
|
|
213
|
+
}
|
|
214
|
+
return this._audit.summary();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Export audit log as NDJSON string (Verified mode only).
|
|
219
|
+
*/
|
|
220
|
+
async exportAudit() {
|
|
221
|
+
this._assertOpen();
|
|
222
|
+
if (!this._audit) {
|
|
223
|
+
throw new Error('exportAudit() requires consistency: "verified"');
|
|
224
|
+
}
|
|
225
|
+
return this._audit.export();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Database metadata and stats.
|
|
230
|
+
*/
|
|
231
|
+
async stats() {
|
|
232
|
+
this._assertOpen();
|
|
233
|
+
return this._store.stats();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Close the database and release file handles.
|
|
238
|
+
*/
|
|
239
|
+
async close() {
|
|
240
|
+
if (this._closed) return;
|
|
241
|
+
this._store.close();
|
|
242
|
+
if (this._audit) this._audit.close();
|
|
243
|
+
this._closed = true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─────────────────────────────────────────
|
|
247
|
+
// Convenience / fluent helpers
|
|
248
|
+
// ─────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Add multiple memories at once.
|
|
252
|
+
*
|
|
253
|
+
* @param {string[]} items
|
|
254
|
+
*/
|
|
255
|
+
async rememberAll(items) {
|
|
256
|
+
this._assertOpen();
|
|
257
|
+
if (!Array.isArray(items)) throw new TypeError('rememberAll() expects an array');
|
|
258
|
+
const results = [];
|
|
259
|
+
for (const item of items) {
|
|
260
|
+
results.push(await this.remember(item));
|
|
261
|
+
}
|
|
262
|
+
return results;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check if any memory semantically matches a query.
|
|
267
|
+
*
|
|
268
|
+
* @param {string} query
|
|
269
|
+
* @returns {boolean}
|
|
270
|
+
*/
|
|
271
|
+
async has(query) {
|
|
272
|
+
const result = await this.recall(query);
|
|
273
|
+
return result !== null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Return the number of stored memories.
|
|
278
|
+
*/
|
|
279
|
+
get size() {
|
|
280
|
+
return this._store.records.length;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Consistency mode of this instance.
|
|
285
|
+
*/
|
|
286
|
+
get consistency() {
|
|
287
|
+
return this._store.header.consistency;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* File path of the underlying .mumpix file.
|
|
292
|
+
*/
|
|
293
|
+
get path() {
|
|
294
|
+
return this._store.filePath;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─────────────────────────────────────────
|
|
298
|
+
// Private
|
|
299
|
+
// ─────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
_assertOpen() {
|
|
302
|
+
if (this._closed) throw new Error('Database is closed. Call Mumpix.open() again.');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = { MumpixDB };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MumpixAudit — immutable append-only audit log for Verified consistency mode
|
|
5
|
+
*
|
|
6
|
+
* Stored alongside the main file: agent.mumpix.audit
|
|
7
|
+
* Each line is a JSON entry — never modified, only appended.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const AUDIT_SCHEMA_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
class MumpixAudit {
|
|
16
|
+
constructor(dbPath) {
|
|
17
|
+
this.auditPath = path.resolve(dbPath) + '.audit';
|
|
18
|
+
this._entries = [];
|
|
19
|
+
this._fd = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
open() {
|
|
23
|
+
if (fs.existsSync(this.auditPath)) {
|
|
24
|
+
this._load();
|
|
25
|
+
} else {
|
|
26
|
+
// Write header
|
|
27
|
+
const header = {
|
|
28
|
+
_type: 'audit_header',
|
|
29
|
+
version: AUDIT_SCHEMA_VERSION,
|
|
30
|
+
created: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
fs.writeFileSync(this.auditPath, JSON.stringify(header) + '\n', 'utf8');
|
|
33
|
+
}
|
|
34
|
+
this._fd = fs.openSync(this.auditPath, 'a');
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
close() {
|
|
39
|
+
if (this._fd !== null) {
|
|
40
|
+
fs.fdatasyncSync(this._fd);
|
|
41
|
+
fs.closeSync(this._fd);
|
|
42
|
+
this._fd = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Log a write operation.
|
|
48
|
+
*/
|
|
49
|
+
logWrite(record) {
|
|
50
|
+
this._append({
|
|
51
|
+
_type: 'write',
|
|
52
|
+
id: record.id,
|
|
53
|
+
hash: record.h,
|
|
54
|
+
len: record.content.length,
|
|
55
|
+
ts: Date.now(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Log a recall operation.
|
|
61
|
+
*/
|
|
62
|
+
logRecall(query, result, latencyMs) {
|
|
63
|
+
this._append({
|
|
64
|
+
_type: 'recall',
|
|
65
|
+
queryHash: this._hash(query),
|
|
66
|
+
queryLen: query.length,
|
|
67
|
+
resultId: result ? result.id : null,
|
|
68
|
+
resultHash: result ? result.h : null,
|
|
69
|
+
score: result ? result._score : null,
|
|
70
|
+
latencyMs: Math.round(latencyMs * 100) / 100,
|
|
71
|
+
ts: Date.now(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Log a clear operation.
|
|
77
|
+
*/
|
|
78
|
+
logClear(count) {
|
|
79
|
+
this._append({
|
|
80
|
+
_type: 'clear',
|
|
81
|
+
count,
|
|
82
|
+
ts: Date.now(),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Log a consistency mode change.
|
|
88
|
+
*/
|
|
89
|
+
logModeChange(from, to) {
|
|
90
|
+
this._append({
|
|
91
|
+
_type: 'mode_change',
|
|
92
|
+
from,
|
|
93
|
+
to,
|
|
94
|
+
ts: Date.now(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Return all audit entries (excluding header).
|
|
100
|
+
*/
|
|
101
|
+
all() {
|
|
102
|
+
return this._entries.map(e => ({ ...e }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Return entries filtered by type.
|
|
107
|
+
*/
|
|
108
|
+
filter(type) {
|
|
109
|
+
return this._entries.filter(e => e._type === type).map(e => ({ ...e }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Return a compact summary.
|
|
114
|
+
*/
|
|
115
|
+
summary() {
|
|
116
|
+
const writes = this._entries.filter(e => e._type === 'write').length;
|
|
117
|
+
const recalls = this._entries.filter(e => e._type === 'recall').length;
|
|
118
|
+
const clears = this._entries.filter(e => e._type === 'clear').length;
|
|
119
|
+
const first = this._entries[0];
|
|
120
|
+
const last = this._entries[this._entries.length - 1];
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
totalEntries: this._entries.length,
|
|
124
|
+
writes,
|
|
125
|
+
recalls,
|
|
126
|
+
clears,
|
|
127
|
+
firstEventAt: first ? first.ts : null,
|
|
128
|
+
lastEventAt: last ? last.ts : null,
|
|
129
|
+
auditPath: this.auditPath,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Export as NDJSON string (for compliance hand-off).
|
|
135
|
+
*/
|
|
136
|
+
export() {
|
|
137
|
+
return this._entries.map(e => JSON.stringify(e)).join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Private ────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
_append(entry) {
|
|
143
|
+
const line = JSON.stringify(entry) + '\n';
|
|
144
|
+
fs.writeSync(this._fd, line, null, 'utf8');
|
|
145
|
+
fs.fdatasyncSync(this._fd);
|
|
146
|
+
this._entries.push(entry);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_load() {
|
|
150
|
+
const raw = fs.readFileSync(this.auditPath, 'utf8');
|
|
151
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
152
|
+
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
try {
|
|
155
|
+
const entry = JSON.parse(line);
|
|
156
|
+
if (entry._type && entry._type !== 'audit_header') {
|
|
157
|
+
this._entries.push(entry);
|
|
158
|
+
}
|
|
159
|
+
} catch (_) { /* skip corrupt lines */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_hash(s) {
|
|
164
|
+
let h = 0x811c9dc5;
|
|
165
|
+
for (let i = 0; i < s.length; i++) {
|
|
166
|
+
h ^= s.charCodeAt(i);
|
|
167
|
+
h = Math.imul(h, 0x01000193);
|
|
168
|
+
}
|
|
169
|
+
return '0x' + (h >>> 0).toString(16).padStart(8, '0');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { MumpixAudit };
|