enigma-memory 0.1.13 → 0.1.15

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 (32) hide show
  1. package/README.md +36 -17
  2. package/apps/cli/bin/enigma.mjs +3126 -2848
  3. package/deploy/docker-compose.local-production-simulation.yml +12 -10
  4. package/docs/benchmark-attestation-network.md +487 -487
  5. package/docs/benchmark-reproducibility.md +228 -227
  6. package/docs/demo-proof-network.md +275 -275
  7. package/docs/developer-ecosystem.md +223 -223
  8. package/docs/developer-proof-quickstart.md +325 -325
  9. package/docs/enigma-memory-ready-conformance.md +376 -376
  10. package/docs/hosted-cloud-product.md +10 -0
  11. package/docs/install-anywhere.md +34 -17
  12. package/docs/installers-and-desktop.md +9 -7
  13. package/docs/proof-network-build-notes.md +240 -240
  14. package/docs/proof-network.md +257 -257
  15. package/docs/sdk-api.md +324 -324
  16. package/docs/solana-devnet-acceptance.md +48 -0
  17. package/docs/solana-proof-rail.md +453 -453
  18. package/examples/ci/github-actions.yml +6 -8
  19. package/package.json +272 -265
  20. package/packages/mcp-server/src/index.js +1185 -1185
  21. package/packages/passport/src/index.js +9 -5
  22. package/scripts/build-benchmark-proof-release.mjs +391 -0
  23. package/scripts/build-goal-completion-audit.mjs +11 -5
  24. package/scripts/build-hosted-api-key-lifecycle.mjs +274 -274
  25. package/scripts/build-hosted-customer-lifecycle.mjs +456 -456
  26. package/scripts/build-installer-assets.mjs +389 -273
  27. package/scripts/build-production-handoff-packet.mjs +7 -6
  28. package/scripts/build-production-unblocker.mjs +409 -0
  29. package/scripts/build-proof-network-packet.mjs +213 -213
  30. package/scripts/release-audit.mjs +71 -2
  31. package/scripts/run-standard-memory-benchmarks.mjs +1070 -1070
  32. package/scripts/wait-for-backend-ready.mjs +4 -2
@@ -1,1070 +1,1070 @@
1
- #!/usr/bin/env node
2
- import { createReadStream } from 'node:fs';
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
- import { basename, dirname, resolve } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { createHash } from 'node:crypto';
7
- import { performance } from 'node:perf_hooks';
8
- import { StringDecoder } from 'node:string_decoder';
9
- import { estimateTextTokens } from '../packages/optimizer/src/index.js';
10
-
11
- export const STANDARD_MEMORY_BENCHMARK_SUITE_SCHEMA = 'enigma.standard_memory_benchmark_suite.v1';
12
-
13
- export const STANDARD_MEMORY_BENCHMARK_METHODS = Object.freeze([
14
- Object.freeze({
15
- id: 'full_context',
16
- label: 'Full context',
17
- boundary: 'Supplies every parsed memory record for each query; no provider API or model answer generation.',
18
- uses_top_k: false,
19
- }),
20
- Object.freeze({
21
- id: 'recency_last_n',
22
- label: 'Recency last N',
23
- boundary: 'Supplies the most recent local memory records up to --top-k.',
24
- uses_top_k: true,
25
- }),
26
- Object.freeze({
27
- id: 'keyword_filter',
28
- label: 'Keyword filter',
29
- boundary: 'Supplies local memory records whose public-safe deterministic tokens overlap the query, capped by --top-k.',
30
- uses_top_k: true,
31
- }),
32
- Object.freeze({
33
- id: 'enigma_relevance',
34
- label: 'Enigma relevance',
35
- boundary: 'Uses deterministic query-aware relevance features over local public-safe memory metadata and content tokens, then ranks locally without provider APIs.',
36
- uses_top_k: true,
37
- }),
38
- ]);
39
-
40
- const LOCOMO_SOURCE_URL = 'https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json';
41
- const LONGMEMEVAL_SOURCE_URLS = Object.freeze([
42
- 'https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_oracle.json',
43
- 'https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json',
44
- 'https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_m_cleaned.json',
45
- ]);
46
-
47
- const QUERY_RELEVANCE_STOPWORDS = new Set([
48
- 'about',
49
- 'after',
50
- 'again',
51
- 'against',
52
- 'also',
53
- 'and',
54
- 'any',
55
- 'are',
56
- 'assistant',
57
- 'because',
58
- 'been',
59
- 'before',
60
- 'being',
61
- 'between',
62
- 'can',
63
- 'could',
64
- 'current',
65
- 'does',
66
- 'from',
67
- 'has',
68
- 'have',
69
- 'how',
70
- 'into',
71
- 'its',
72
- 'latest',
73
- 'more',
74
- 'most',
75
- 'number',
76
- 'own',
77
- 'owns',
78
- 'please',
79
- 'should',
80
- 'that',
81
- 'the',
82
- 'their',
83
- 'then',
84
- 'there',
85
- 'these',
86
- 'they',
87
- 'this',
88
- 'use',
89
- 'using',
90
- 'was',
91
- 'what',
92
- 'when',
93
- 'where',
94
- 'which',
95
- 'who',
96
- 'whose',
97
- 'why',
98
- 'with',
99
- 'would',
100
- ]);
101
-
102
- function parseArgs(argv = process.argv.slice(2)) {
103
- const options = { top_k: 5 };
104
- for (let index = 0; index < argv.length; index += 1) {
105
- const arg = argv[index];
106
- if (arg === '--locomo') {
107
- options.locomo = requiredFlagValue(argv, index, arg);
108
- index += 1;
109
- } else if (arg === '--longmemeval') {
110
- options.longmemeval = requiredFlagValue(argv, index, arg);
111
- index += 1;
112
- } else if (arg === '--max-locomo-qa') {
113
- options.max_locomo_qa = positiveInteger(requiredFlagValue(argv, index, arg), arg);
114
- index += 1;
115
- } else if (arg === '--max-longmemeval-items') {
116
- options.max_longmemeval_items = positiveInteger(requiredFlagValue(argv, index, arg), arg);
117
- index += 1;
118
- } else if (arg === '--top-k') {
119
- options.top_k = positiveInteger(requiredFlagValue(argv, index, arg), arg);
120
- index += 1;
121
- } else if (arg === '--out') {
122
- options.out = requiredFlagValue(argv, index, arg);
123
- index += 1;
124
- } else if (arg === '--help' || arg === '-h') {
125
- options.help = true;
126
- } else {
127
- throw new Error(`Unknown option ${arg}`);
128
- }
129
- }
130
- return options;
131
- }
132
-
133
- function requiredFlagValue(argv, index, flag) {
134
- const value = argv[index + 1];
135
- if (value === undefined || value.startsWith('--')) throw new Error(`${flag} requires a value`);
136
- return value;
137
- }
138
-
139
- function positiveInteger(value, name) {
140
- const number = Number(value);
141
- if (!Number.isInteger(number) || number <= 0) throw new Error(`${name} must be a positive integer`);
142
- return number;
143
- }
144
-
145
- function optionalPositiveInteger(value, name) {
146
- if (value === undefined || value === null) return undefined;
147
- return positiveInteger(value, name);
148
- }
149
-
150
- function publicFileName(path) {
151
- return path === undefined || path === null ? undefined : basename(String(path));
152
- }
153
-
154
- async function readJsonWithSha256(path) {
155
- const raw = await readFile(path, 'utf8');
156
- return {
157
- data: JSON.parse(raw),
158
- sha256: createHash('sha256').update(raw).digest('hex'),
159
- };
160
- }
161
-
162
- function isJsonWhitespace(char) {
163
- const code = char.charCodeAt(0);
164
- return code === 0x20 || code === 0x0a || code === 0x0d || code === 0x09 || code === 0xfeff;
165
- }
166
-
167
- function createTopLevelArraySampler(maxItems, name) {
168
- const items = [];
169
- let started = false;
170
- let closed = false;
171
- let collecting = false;
172
- let doneCollecting = false;
173
- let expectSeparator = false;
174
- let depth = 0;
175
- let inString = false;
176
- let escaped = false;
177
- let current = '';
178
-
179
- function fail(message) {
180
- throw new Error(`${name} sample-mode JSON parse failed: ${message}`);
181
- }
182
-
183
- function finishItem() {
184
- items.push(current);
185
- current = '';
186
- collecting = false;
187
- expectSeparator = true;
188
- if (items.length >= maxItems) doneCollecting = true;
189
- }
190
-
191
- return {
192
- write(text) {
193
- for (const char of text) {
194
- if (!started) {
195
- if (isJsonWhitespace(char)) continue;
196
- if (char !== '[') fail('expected a top-level array');
197
- started = true;
198
- continue;
199
- }
200
-
201
- if (doneCollecting) continue;
202
-
203
- if (closed) {
204
- if (!isJsonWhitespace(char)) fail('found trailing data after the top-level array');
205
- continue;
206
- }
207
-
208
- if (!collecting) {
209
- if (isJsonWhitespace(char)) continue;
210
- if (expectSeparator) {
211
- if (char === ',') {
212
- expectSeparator = false;
213
- continue;
214
- }
215
- if (char === ']') {
216
- closed = true;
217
- continue;
218
- }
219
- fail('expected a comma or closing bracket between items');
220
- }
221
- if (char === ']') {
222
- closed = true;
223
- continue;
224
- }
225
- if (char !== '{' && char !== '[') fail('expected each sampled item to be an object or array');
226
- collecting = true;
227
- current = char;
228
- depth = 1;
229
- continue;
230
- }
231
-
232
- current += char;
233
- if (inString) {
234
- if (escaped) {
235
- escaped = false;
236
- } else if (char === '\\') {
237
- escaped = true;
238
- } else if (char === '"') {
239
- inString = false;
240
- }
241
- continue;
242
- }
243
- if (char === '"') {
244
- inString = true;
245
- } else if (char === '{' || char === '[') {
246
- depth += 1;
247
- } else if (char === '}' || char === ']') {
248
- depth -= 1;
249
- if (depth < 0) fail('encountered an unmatched closing bracket');
250
- if (depth === 0) finishItem();
251
- }
252
- }
253
- },
254
- get done() {
255
- return doneCollecting;
256
- },
257
- finish() {
258
- if (!started) fail('empty input');
259
- if (!doneCollecting && collecting) fail('ended inside a sampled item');
260
- if (!doneCollecting && inString) fail('ended inside a string');
261
- if (!doneCollecting && !closed) fail('ended before the top-level array closed');
262
- return JSON.parse(`[${items.join(',')}]`);
263
- },
264
- };
265
- }
266
-
267
- async function readJsonArraySampleWithSha256(path, maxItems, name) {
268
- const hash = createHash('sha256');
269
- const decoder = new StringDecoder('utf8');
270
- const sampler = createTopLevelArraySampler(maxItems, name);
271
- for await (const chunk of createReadStream(path)) {
272
- hash.update(chunk);
273
- if (!sampler.done) sampler.write(decoder.write(chunk));
274
- }
275
- if (!sampler.done) {
276
- const tail = decoder.end();
277
- if (tail.length > 0) sampler.write(tail);
278
- }
279
- return {
280
- data: sampler.finish(),
281
- sha256: hash.digest('hex'),
282
- };
283
- }
284
-
285
- async function readLongMemEvalJsonWithSha256(path, maxItems) {
286
- if (maxItems === undefined) return readJsonWithSha256(path);
287
- try {
288
- return await readJsonArraySampleWithSha256(path, maxItems, 'LongMemEval');
289
- } catch (error) {
290
- if (error instanceof Error && error.message === 'LongMemEval sample-mode JSON parse failed: expected a top-level array') {
291
- return readJsonWithSha256(path);
292
- }
293
- throw error;
294
- }
295
- }
296
-
297
- function normalizeDatasetArray(data, name) {
298
- if (Array.isArray(data)) return data;
299
- if (data && typeof data === 'object') {
300
- for (const key of ['data', 'items', 'examples', 'samples']) {
301
- if (Array.isArray(data[key])) return data[key];
302
- }
303
- }
304
- throw new TypeError(`${name} dataset must be a JSON array or object containing an array`);
305
- }
306
-
307
- function addMeaningfulToken(tokens, token) {
308
- if (token.length < 3) return;
309
- if (!/[a-z]/u.test(token)) return;
310
- if (QUERY_RELEVANCE_STOPWORDS.has(token)) return;
311
- tokens.add(token);
312
- }
313
-
314
- function stemToken(token) {
315
- if (token.length > 5 && token.endsWith('ing')) {
316
- let stem = token.slice(0, -3);
317
- if (stem.length > 3 && stem.at(-1) === stem.at(-2)) stem = stem.slice(0, -1);
318
- return stem;
319
- }
320
- if (token.length > 4 && token.endsWith('ed')) {
321
- let stem = token.slice(0, -2);
322
- if (stem.length > 3 && stem.at(-1) === stem.at(-2)) stem = stem.slice(0, -1);
323
- return stem;
324
- }
325
- if (token.length > 4 && token.endsWith('ies')) return `${token.slice(0, -3)}y`;
326
- if (token.length > 4 && token.endsWith('es')) return token.slice(0, -2);
327
- if (token.length > 3 && token.endsWith('s') && !token.endsWith('ss')) return token.slice(0, -1);
328
- return token;
329
- }
330
-
331
- function addStemmedMeaningfulToken(tokens, token) {
332
- addMeaningfulToken(tokens, token);
333
- const stem = stemToken(token);
334
- addMeaningfulToken(tokens, stem);
335
- if (token.endsWith('ed') || token.endsWith('ing')) addMeaningfulToken(tokens, `${stem}e`);
336
- }
337
-
338
- function meaningfulTokensFrom(value) {
339
- const tokens = new Set();
340
- if (value === undefined || value === null) return tokens;
341
- for (const match of String(value).toLowerCase().matchAll(/[a-z0-9]+(?:[-_][a-z0-9]+)*/gu)) {
342
- const token = match[0];
343
- addMeaningfulToken(tokens, token);
344
- if (token.includes('-') || token.includes('_')) {
345
- for (const part of token.split(/[-_]+/u)) addMeaningfulToken(tokens, part);
346
- }
347
- }
348
- return tokens;
349
- }
350
-
351
- function stemmedMeaningfulTokensFrom(value) {
352
- const tokens = new Set();
353
- if (value === undefined || value === null) return tokens;
354
- for (const match of String(value).toLowerCase().matchAll(/[a-z0-9]+(?:[-_][a-z0-9]+)*/gu)) {
355
- const token = match[0];
356
- addStemmedMeaningfulToken(tokens, token);
357
- if (token.includes('-') || token.includes('_')) {
358
- for (const part of token.split(/[-_]+/u)) addStemmedMeaningfulToken(tokens, part);
359
- }
360
- }
361
- return tokens;
362
- }
363
-
364
- function stemmedTokenSequenceFrom(value) {
365
- const sequence = [];
366
- if (value === undefined || value === null) return sequence;
367
- for (const match of String(value).toLowerCase().matchAll(/[a-z0-9]+(?:[-_][a-z0-9]+)*/gu)) {
368
- const token = match[0];
369
- const parts = token.includes('-') || token.includes('_') ? token.split(/[-_]+/u) : [token];
370
- for (const part of parts) {
371
- const stem = stemToken(part);
372
- if (stem.length >= 3 && /[a-z]/u.test(stem) && !QUERY_RELEVANCE_STOPWORDS.has(stem)) sequence.push(stem);
373
- }
374
- }
375
- return sequence;
376
- }
377
-
378
- function tokenOverlapScore(queryTokens, record) {
379
- if (queryTokens.size === 0) return 0;
380
- const recordTokens = meaningfulTokensFrom(record.content);
381
- let score = 0;
382
- for (const token of queryTokens) if (recordTokens.has(token)) score += 1;
383
- return score;
384
- }
385
-
386
- function normalizedTagSessionToken(value) {
387
- return String(value ?? '').toLowerCase().replace(/^session[-_:]?/u, '').replace(/[^a-z0-9]+/gu, '');
388
- }
389
-
390
- function roleHintsFrom(value) {
391
- const hints = new Set();
392
- const text = String(value ?? '').toLowerCase();
393
- for (const match of text.matchAll(/\b(?:assistant|user|system|human|agent|speaker[-_\s]?[a-z0-9]+)\b/gu)) {
394
- const compact = match[0].replace(/\s+/gu, '_');
395
- hints.add(compact);
396
- for (const token of stemmedMeaningfulTokensFrom(compact)) hints.add(token);
397
- }
398
- return hints;
399
- }
400
-
401
- function sessionHintsFrom(value) {
402
- const hints = new Set();
403
- const text = String(value ?? '').toLowerCase();
404
- for (const match of text.matchAll(/\b(?:session|sess)\s*[-_:]?\s*([a-z0-9]+)\b/gu)) hints.add(match[1]);
405
- for (const match of text.matchAll(/\bd\s*[-_:]?\s*(\d+)\b/gu)) hints.add(`d${Number(match[1])}`);
406
- for (const match of text.matchAll(/\bsession[-_]([a-z0-9]+)\b/gu)) hints.add(match[1]);
407
- return hints;
408
- }
409
-
410
- function dateHintsFrom(value) {
411
- const hints = new Set();
412
- const text = String(value ?? '').toLowerCase();
413
- for (const match of text.matchAll(/\b(?:19|20)\d{2}\b/gu)) hints.add(match[0]);
414
- for (const match of text.matchAll(/\b\d{4}[-/]\d{1,2}(?:[-/]\d{1,2})?\b/gu)) hints.add(match[0].replace(/\D+/gu, '-'));
415
- for (const match of text.matchAll(/\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b/gu)) hints.add(match[0].slice(0, 3));
416
- for (const match of text.matchAll(/\b(?:today|yesterday|tomorrow|recent|recently|latest|newest|current|previous|last|earliest|oldest)\b/gu)) hints.add(match[0]);
417
- return hints;
418
- }
419
-
420
- function recordSessionTokens(record) {
421
- const tokens = new Set();
422
- const values = [record.session_id, record.dialog_id, record.turn_id, ...(record.tags ?? [])];
423
- for (const value of values) {
424
- if (value === undefined || value === null) continue;
425
- const normalized = normalizedTagSessionToken(value);
426
- if (normalized) tokens.add(normalized);
427
- for (const hint of sessionHintsFrom(value)) tokens.add(normalizedTagSessionToken(hint));
428
- }
429
- return tokens;
430
- }
431
-
432
- function countSetIntersection(left, right) {
433
- let count = 0;
434
- for (const value of left) if (right.has(value)) count += 1;
435
- return count;
436
- }
437
-
438
- function phraseAndProximityScore(querySequence, recordSequence) {
439
- if (querySequence.length < 2 || recordSequence.length < 2) return 0;
440
- const recordBigrams = new Set();
441
- const positions = new Map();
442
- for (let index = 0; index < recordSequence.length; index += 1) {
443
- const token = recordSequence[index];
444
- if (!positions.has(token)) positions.set(token, []);
445
- positions.get(token).push(index);
446
- if (index > 0) recordBigrams.add(`${recordSequence[index - 1]}\u0000${token}`);
447
- }
448
- let score = 0;
449
- for (let index = 1; index < querySequence.length; index += 1) {
450
- const previous = querySequence[index - 1];
451
- const current = querySequence[index];
452
- if (previous === current) continue;
453
- if (recordBigrams.has(`${previous}\u0000${current}`)) {
454
- score += 10;
455
- continue;
456
- }
457
- const leftPositions = positions.get(previous);
458
- const rightPositions = positions.get(current);
459
- if (!leftPositions || !rightPositions) continue;
460
- let near = false;
461
- for (const left of leftPositions) {
462
- for (const right of rightPositions) {
463
- if (Math.abs(left - right) <= 6) {
464
- near = true;
465
- break;
466
- }
467
- }
468
- if (near) break;
469
- }
470
- if (near) score += 4;
471
- }
472
- return score;
473
- }
474
-
475
- function hasRecencyIntent(hints) {
476
- for (const hint of hints) {
477
- if (hint === 'recent' || hint === 'recently' || hint === 'latest' || hint === 'newest' || hint === 'current' || hint === 'previous' || hint === 'last') return true;
478
- }
479
- return false;
480
- }
481
-
482
- function enigmaRelevanceScore(query, record) {
483
- const question = String(query.question ?? '');
484
- const queryTokens = stemmedMeaningfulTokensFrom(question);
485
- const categoryTokens = stemmedMeaningfulTokensFrom(`${query.category ?? ''} ${query.question_type ?? ''}`);
486
- const contentTokens = stemmedMeaningfulTokensFrom(record.content);
487
- const metadataTokens = stemmedMeaningfulTokensFrom(`${record.kind ?? ''} ${(record.tags ?? []).join(' ')}`);
488
- for (const token of stemmedMeaningfulTokensFrom(`${record.role ?? ''} ${record.session_id ?? ''} ${record.dialog_id ?? ''} ${record.turn_id ?? ''}`)) {
489
- metadataTokens.add(token);
490
- }
491
-
492
- const contentMatches = countSetIntersection(queryTokens, contentTokens);
493
- const metadataMatches = countSetIntersection(queryTokens, metadataTokens);
494
- const categoryMatches = countSetIntersection(categoryTokens, metadataTokens) + countSetIntersection(categoryTokens, contentTokens);
495
- const roleMatches = countSetIntersection(roleHintsFrom(question), roleHintsFrom(record.role));
496
- const querySessionHints = sessionHintsFrom(question);
497
- const recordSessions = recordSessionTokens(record);
498
- const sessionMatches = countSetIntersection(querySessionHints, recordSessions);
499
- const queryDateHints = dateHintsFrom(question);
500
- const temporalMatches = countSetIntersection(queryDateHints, dateHintsFrom(`${record.content} ${(record.tags ?? []).join(' ')}`));
501
- const temporalRecencyScore = hasRecencyIntent(queryDateHints) ? Math.min(6, Math.log2(record.ordinal + 2)) : 0;
502
- const phraseScore = phraseAndProximityScore(stemmedTokenSequenceFrom(question), stemmedTokenSequenceFrom(record.content));
503
-
504
- return {
505
- record,
506
- score: (contentMatches * 12)
507
- + (metadataMatches * 4)
508
- + (categoryMatches * 3)
509
- + (roleMatches * 18)
510
- + (sessionMatches * 16)
511
- + (temporalMatches * 8)
512
- + temporalRecencyScore
513
- + phraseScore,
514
- contentMatches,
515
- metadataMatches,
516
- categoryMatches,
517
- roleMatches,
518
- sessionMatches,
519
- temporalMatches,
520
- temporalRecencyScore,
521
- phraseScore,
522
- };
523
- }
524
-
525
- function compareRecordId(left, right) {
526
- return String(left.id).localeCompare(String(right.id));
527
- }
528
-
529
- function compareRecencyDesc(left, right) {
530
- if (left.ordinal !== right.ordinal) return right.ordinal - left.ordinal;
531
- return compareRecordId(left, right);
532
- }
533
-
534
- function rankedByOverlap(records, query) {
535
- const queryTokens = meaningfulTokensFrom(query);
536
- if (queryTokens.size === 0) return [];
537
- const scored = [];
538
- for (const record of records) {
539
- const score = tokenOverlapScore(queryTokens, record);
540
- if (score > 0) scored.push({ record, score });
541
- }
542
- scored.sort((left, right) => {
543
- if (left.score !== right.score) return right.score - left.score;
544
- return compareRecencyDesc(left.record, right.record);
545
- });
546
- return scored.map((item) => item.record);
547
- }
548
-
549
- function rankedByEnigmaRelevance(records, query) {
550
- const scored = [];
551
- for (const record of records) {
552
- const item = enigmaRelevanceScore(query, record);
553
- if (item.score > 0) scored.push(item);
554
- }
555
- if (scored.length === 0) return [...records].sort(compareRecencyDesc);
556
- scored.sort((left, right) => {
557
- if (left.score !== right.score) return right.score - left.score;
558
- if (left.phraseScore !== right.phraseScore) return right.phraseScore - left.phraseScore;
559
- if (left.contentMatches !== right.contentMatches) return right.contentMatches - left.contentMatches;
560
- if (left.roleMatches !== right.roleMatches) return right.roleMatches - left.roleMatches;
561
- if (left.sessionMatches !== right.sessionMatches) return right.sessionMatches - left.sessionMatches;
562
- if (left.temporalMatches !== right.temporalMatches) return right.temporalMatches - left.temporalMatches;
563
- if (left.temporalRecencyScore !== right.temporalRecencyScore) return right.temporalRecencyScore - left.temporalRecencyScore;
564
- if (left.metadataMatches !== right.metadataMatches) return right.metadataMatches - left.metadataMatches;
565
- return compareRecencyDesc(left.record, right.record);
566
- });
567
- return scored.map((item) => item.record);
568
- }
569
-
570
- function selectRecords(methodId, records, query, topK) {
571
- if (methodId === 'full_context') return records;
572
- if (methodId === 'recency_last_n') return [...records].sort(compareRecencyDesc).slice(0, topK);
573
- if (methodId === 'keyword_filter') return rankedByOverlap(records, query.question).slice(0, topK);
574
- if (methodId === 'enigma_relevance') return rankedByEnigmaRelevance(records, query).slice(0, topK);
575
- throw new Error(`Unknown method ${methodId}`);
576
- }
577
-
578
- function estimatePromptTokens(question, selectedRecords) {
579
- let tokens = estimateTextTokens(question);
580
- for (const record of selectedRecords) tokens += record.estimated_tokens;
581
- return tokens;
582
- }
583
-
584
- function percentile(values, ratio) {
585
- if (values.length === 0) return 0;
586
- const sorted = [...values].sort((left, right) => left - right);
587
- const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1));
588
- return sorted[index];
589
- }
590
-
591
- function latencySummary(values) {
592
- return {
593
- samples: values.length,
594
- p50_ms: round6(percentile(values, 0.5)),
595
- p95_ms: round6(percentile(values, 0.95)),
596
- min_ms: round6(values.length === 0 ? 0 : Math.min(...values)),
597
- max_ms: round6(values.length === 0 ? 0 : Math.max(...values)),
598
- };
599
- }
600
-
601
- function rate(numerator, denominator) {
602
- if (denominator === 0) return null;
603
- return round6(numerator / denominator);
604
- }
605
-
606
- function round6(value) {
607
- return Number(value.toFixed(6));
608
- }
609
-
610
- function mean(total, count) {
611
- return count === 0 ? 0 : round6(total / count);
612
- }
613
-
614
- function recordFromContent(args) {
615
- const content = String(args.content ?? '');
616
- return {
617
- id: args.id,
618
- dataset_item_id: args.dataset_item_id,
619
- session_id: args.session_id,
620
- turn_id: args.turn_id,
621
- dialog_id: args.dialog_id,
622
- role: args.role,
623
- kind: args.kind,
624
- tags: args.tags ?? [],
625
- has_answer: args.has_answer === true,
626
- ordinal: args.ordinal,
627
- content,
628
- estimated_tokens: estimateTextTokens(content),
629
- };
630
- }
631
-
632
- function parseLocomoEvidenceLabels(evidence) {
633
- const labels = new Set();
634
- const stack = Array.isArray(evidence) ? [...evidence] : [evidence];
635
- while (stack.length > 0) {
636
- const value = stack.shift();
637
- if (Array.isArray(value)) {
638
- stack.push(...value);
639
- continue;
640
- }
641
- if (value === undefined || value === null) continue;
642
- for (const match of String(value).matchAll(/D\s*(\d+)\s*:\s*(\d+)/giu)) {
643
- labels.add(`D${Number(match[1])}:${Number(match[2])}`);
644
- }
645
- }
646
- return labels;
647
- }
648
-
649
- function dialogIdForTurn(sessionNumber, turn, turnIndex) {
650
- const raw = turn?.dia_id ?? turn?.dialog_id ?? turn?.turn_id ?? turn?.id ?? turnIndex + 1;
651
- const text = String(raw);
652
- const match = text.match(/^D\s*(\d+)\s*:\s*(\d+)$/iu);
653
- if (match) return `D${Number(match[1])}:${Number(match[2])}`;
654
- const numeric = text.match(/\d+/u)?.[0] ?? String(turnIndex + 1);
655
- return `D${sessionNumber}:${Number(numeric)}`;
656
- }
657
-
658
- export function parseLocomoDataset(data, options = {}) {
659
- const maxQa = optionalPositiveInteger(options.max_qa ?? options.maxQa, 'max_locomo_qa');
660
- const rows = normalizeDatasetArray(data, 'LoCoMo');
661
- const records = [];
662
- const queries = [];
663
- let ordinal = 0;
664
- for (let sampleIndex = 0; sampleIndex < rows.length; sampleIndex += 1) {
665
- const sample = rows[sampleIndex] ?? {};
666
- const itemId = String(sample.sample_id ?? sample.id ?? `sample_${sampleIndex + 1}`);
667
- const conversation = sample.conversation ?? {};
668
- const sessionNames = Object.keys(conversation)
669
- .map((key) => {
670
- const match = key.match(/^session_(\d+)$/u);
671
- return match ? { key, sessionNumber: Number(match[1]) } : null;
672
- })
673
- .filter(Boolean)
674
- .sort((left, right) => left.sessionNumber - right.sessionNumber);
675
- for (const { key, sessionNumber } of sessionNames) {
676
- const session = conversation[key];
677
- if (!Array.isArray(session)) continue;
678
- for (let turnIndex = 0; turnIndex < session.length; turnIndex += 1) {
679
- const turn = session[turnIndex] ?? {};
680
- const dialogId = dialogIdForTurn(sessionNumber, turn, turnIndex);
681
- records.push(recordFromContent({
682
- id: `locomo:${itemId}:${dialogId}`,
683
- dataset_item_id: itemId,
684
- session_id: `D${sessionNumber}`,
685
- turn_id: dialogId,
686
- dialog_id: dialogId,
687
- role: turn.speaker ?? turn.role ?? undefined,
688
- kind: 'locomo_conversation_turn',
689
- tags: ['locomo', `session_${sessionNumber}`],
690
- ordinal,
691
- content: turn.text ?? turn.content ?? turn.message ?? '',
692
- }));
693
- ordinal += 1;
694
- }
695
- }
696
- const qaRows = Array.isArray(sample.qa) ? sample.qa : [];
697
- for (let qaIndex = 0; qaIndex < qaRows.length; qaIndex += 1) {
698
- if (maxQa !== undefined && queries.length >= maxQa) break;
699
- const qa = qaRows[qaIndex] ?? {};
700
- const evidenceDialogIds = parseLocomoEvidenceLabels(qa.evidence);
701
- queries.push({
702
- id: `locomo:${itemId}:qa_${qaIndex + 1}`,
703
- dataset_item_id: itemId,
704
- question: String(qa.question ?? ''),
705
- category: qa.category === undefined ? undefined : String(qa.category),
706
- evidence_dialog_ids: evidenceDialogIds,
707
- abstention: evidenceDialogIds.size === 0,
708
- });
709
- }
710
- if (maxQa !== undefined && queries.length >= maxQa) break;
711
- }
712
- return {
713
- id: 'locomo',
714
- label: 'LoCoMo',
715
- source_url: LOCOMO_SOURCE_URL,
716
- license: 'CC BY-NC 4.0',
717
- parser: 'conversation session turns as memory records; qa evidence labels mapped to dialog ids such as D1:3 and semicolon-separated labels',
718
- task_categories: ['multi-session QA', 'event summarization', 'multimodal generation over long conversations'],
719
- records,
720
- queries,
721
- };
722
- }
723
-
724
- function sessionIdAt(ids, index) {
725
- if (Array.isArray(ids) && ids[index] !== undefined && ids[index] !== null) return String(ids[index]);
726
- return String(index);
727
- }
728
-
729
- function answerSessionIds(item) {
730
- if (!Array.isArray(item.answer_session_ids)) return new Set();
731
- return new Set(item.answer_session_ids.map((id) => String(id)));
732
- }
733
-
734
- function isLongMemEvalAbstention(item, answerSessions) {
735
- const id = String(item.question_id ?? item.id ?? '');
736
- if (id.endsWith('_abs') || id.includes('_abs_')) return true;
737
- if (String(item.question_type ?? '').toLowerCase().includes('abst')) return true;
738
- return answerSessions.size === 0;
739
- }
740
-
741
- export function parseLongMemEvalDataset(data, options = {}) {
742
- const maxItems = optionalPositiveInteger(options.max_items ?? options.maxItems, 'max_longmemeval_items');
743
- const rows = normalizeDatasetArray(data, 'LongMemEval');
744
- const records = [];
745
- const queries = [];
746
- let ordinal = 0;
747
- const limit = maxItems === undefined ? rows.length : Math.min(rows.length, maxItems);
748
- for (let itemIndex = 0; itemIndex < limit; itemIndex += 1) {
749
- const item = rows[itemIndex] ?? {};
750
- const itemId = String(item.question_id ?? item.id ?? `item_${itemIndex + 1}`);
751
- const sessions = Array.isArray(item.haystack_sessions) ? item.haystack_sessions : [];
752
- const sessionIds = item.haystack_session_ids;
753
- const evidenceTurnIds = new Set();
754
- for (let sessionIndex = 0; sessionIndex < sessions.length; sessionIndex += 1) {
755
- const session = sessions[sessionIndex];
756
- if (!Array.isArray(session)) continue;
757
- const sessionId = sessionIdAt(sessionIds, sessionIndex);
758
- for (let turnIndex = 0; turnIndex < session.length; turnIndex += 1) {
759
- const turn = session[turnIndex] ?? {};
760
- const turnId = `${sessionId}:${turnIndex}`;
761
- if (turn.has_answer === true) evidenceTurnIds.add(turnId);
762
- records.push(recordFromContent({
763
- id: `longmemeval:${itemId}:${turnId}`,
764
- dataset_item_id: itemId,
765
- session_id: sessionId,
766
- turn_id: turnId,
767
- role: turn.role ?? undefined,
768
- kind: 'longmemeval_haystack_turn',
769
- tags: ['longmemeval', String(item.question_type ?? ''), `session_${sessionId}`],
770
- has_answer: turn.has_answer === true,
771
- ordinal,
772
- content: turn.content ?? turn.text ?? turn.message ?? '',
773
- }));
774
- ordinal += 1;
775
- }
776
- }
777
- const answerSessions = answerSessionIds(item);
778
- queries.push({
779
- id: `longmemeval:${itemId}`,
780
- dataset_item_id: itemId,
781
- question: String(item.question ?? ''),
782
- question_type: item.question_type === undefined ? undefined : String(item.question_type),
783
- evidence_turn_ids: evidenceTurnIds,
784
- evidence_session_ids: answerSessions,
785
- abstention: isLongMemEvalAbstention(item, answerSessions),
786
- });
787
- }
788
- return {
789
- id: 'longmemeval',
790
- label: 'LongMemEval',
791
- source_url: LONGMEMEVAL_SOURCE_URLS,
792
- license: 'See Hugging Face dataset card and upstream LongMemEval repository for the selected cleaned file.',
793
- parser: 'haystack_sessions turns as memory records; has_answer:true turns and answer_session_ids are used as evidence labels; _abs ids are evaluated as abstention cases',
794
- task_categories: ['information extraction', 'multi-session reasoning', 'temporal reasoning', 'knowledge updates', 'abstention'],
795
- records,
796
- queries,
797
- };
798
- }
799
-
800
- function scoreLocomoMethod(method, dataset, topK) {
801
- const latencies = [];
802
- let evidenceQuestions = 0;
803
- let hits = 0;
804
- let exactCoverage = 0;
805
- let totalTokens = 0;
806
- let selectedTotal = 0;
807
- for (const query of dataset.queries) {
808
- const start = performance.now();
809
- const records = dataset.records.filter((record) => record.dataset_item_id === query.dataset_item_id);
810
- const selected = selectRecords(method.id, records, query, topK);
811
- latencies.push(performance.now() - start);
812
- const selectedDialogs = new Set(selected.map((record) => record.dialog_id));
813
- selectedTotal += selected.length;
814
- totalTokens += estimatePromptTokens(query.question, selected);
815
- if (query.evidence_dialog_ids.size > 0) {
816
- evidenceQuestions += 1;
817
- let covered = 0;
818
- for (const evidenceId of query.evidence_dialog_ids) if (selectedDialogs.has(evidenceId)) covered += 1;
819
- if (covered > 0) hits += 1;
820
- if (covered === query.evidence_dialog_ids.size) exactCoverage += 1;
821
- }
822
- }
823
- return {
824
- id: method.id,
825
- method: method.id,
826
- local_method_only: true,
827
- external_provider_called: false,
828
- retrieval_proxy_only: true,
829
- uses_top_k: method.uses_top_k,
830
- top_k: method.uses_top_k ? topK : null,
831
- question_count: dataset.queries.length,
832
- evidence_question_count: evidenceQuestions,
833
- evidence_hit_at_k: rate(hits, evidenceQuestions),
834
- exact_evidence_coverage: rate(exactCoverage, evidenceQuestions),
835
- estimated_prompt_tokens: {
836
- total: totalTokens,
837
- mean_per_question: mean(totalTokens, dataset.queries.length),
838
- estimator: 'estimateTextTokens deterministic local estimator',
839
- },
840
- selected_memory_count: {
841
- total: selectedTotal,
842
- mean_per_question: mean(selectedTotal, dataset.queries.length),
843
- },
844
- latency: latencySummary(latencies),
845
- public_question_text_included: false,
846
- public_answer_text_included: false,
847
- raw_conversation_text_included: false,
848
- };
849
- }
850
-
851
- function scoreLongMemEvalMethod(method, dataset, topK) {
852
- const latencies = [];
853
- let turnEvidenceQuestions = 0;
854
- let turnHits = 0;
855
- let exactTurnCoverage = 0;
856
- let sessionEvidenceQuestions = 0;
857
- let sessionHits = 0;
858
- let exactSessionCoverage = 0;
859
- let abstentionQuestions = 0;
860
- let abstentionCorrect = 0;
861
- let totalTokens = 0;
862
- let selectedTotal = 0;
863
- for (const query of dataset.queries) {
864
- const start = performance.now();
865
- const records = dataset.records.filter((record) => record.dataset_item_id === query.dataset_item_id);
866
- const selected = selectRecords(method.id, records, query, topK);
867
- latencies.push(performance.now() - start);
868
- const selectedTurns = new Set(selected.map((record) => record.turn_id));
869
- const selectedSessions = new Set(selected.map((record) => record.session_id));
870
- selectedTotal += selected.length;
871
- totalTokens += estimatePromptTokens(query.question, selected);
872
-
873
- if (query.abstention) {
874
- abstentionQuestions += 1;
875
- let selectedGold = false;
876
- for (const turnId of query.evidence_turn_ids) if (selectedTurns.has(turnId)) selectedGold = true;
877
- for (const sessionId of query.evidence_session_ids) if (selectedSessions.has(sessionId)) selectedGold = true;
878
- if (!selectedGold) abstentionCorrect += 1;
879
- continue;
880
- }
881
-
882
- if (query.evidence_turn_ids.size > 0) {
883
- turnEvidenceQuestions += 1;
884
- let covered = 0;
885
- for (const turnId of query.evidence_turn_ids) if (selectedTurns.has(turnId)) covered += 1;
886
- if (covered > 0) turnHits += 1;
887
- if (covered === query.evidence_turn_ids.size) exactTurnCoverage += 1;
888
- }
889
- if (query.evidence_session_ids.size > 0) {
890
- sessionEvidenceQuestions += 1;
891
- let covered = 0;
892
- for (const sessionId of query.evidence_session_ids) if (selectedSessions.has(sessionId)) covered += 1;
893
- if (covered > 0) sessionHits += 1;
894
- if (covered === query.evidence_session_ids.size) exactSessionCoverage += 1;
895
- }
896
- }
897
- return {
898
- id: method.id,
899
- method: method.id,
900
- local_method_only: true,
901
- external_provider_called: false,
902
- retrieval_proxy_only: true,
903
- uses_top_k: method.uses_top_k,
904
- top_k: method.uses_top_k ? topK : null,
905
- item_count: dataset.queries.length,
906
- turn_evidence_question_count: turnEvidenceQuestions,
907
- turn_evidence_hit_at_k: rate(turnHits, turnEvidenceQuestions),
908
- exact_turn_evidence_coverage: rate(exactTurnCoverage, turnEvidenceQuestions),
909
- session_evidence_question_count: sessionEvidenceQuestions,
910
- session_evidence_hit_at_k: rate(sessionHits, sessionEvidenceQuestions),
911
- exact_session_evidence_coverage: rate(exactSessionCoverage, sessionEvidenceQuestions),
912
- abstention_questions: abstentionQuestions,
913
- abstention_correct: abstentionCorrect,
914
- abstention_correctness: rate(abstentionCorrect, abstentionQuestions),
915
- estimated_prompt_tokens: {
916
- total: totalTokens,
917
- mean_per_item: mean(totalTokens, dataset.queries.length),
918
- estimator: 'estimateTextTokens deterministic local estimator',
919
- },
920
- selected_memory_count: {
921
- total: selectedTotal,
922
- mean_per_item: mean(selectedTotal, dataset.queries.length),
923
- },
924
- latency: latencySummary(latencies),
925
- public_question_text_included: false,
926
- public_answer_text_included: false,
927
- raw_conversation_text_included: false,
928
- };
929
- }
930
-
931
- function scoreDataset(dataset, topK) {
932
- const methodRows = STANDARD_MEMORY_BENCHMARK_METHODS.map((method) => (
933
- dataset.id === 'locomo' ? scoreLocomoMethod(method, dataset, topK) : scoreLongMemEvalMethod(method, dataset, topK)
934
- ));
935
- return {
936
- id: dataset.id,
937
- dataset: dataset.id,
938
- label: dataset.label,
939
- source_url: dataset.source_url,
940
- license: dataset.license,
941
- parser: dataset.parser,
942
- task_categories: dataset.task_categories,
943
- record_count: dataset.records.length,
944
- question_count: dataset.queries.length,
945
- item_count: dataset.queries.length,
946
- raw_question_text_included: false,
947
- raw_answer_text_included: false,
948
- raw_conversation_text_included: false,
949
- methods: methodRows,
950
- };
951
- }
952
-
953
- export function runStandardMemoryBenchmarkSuite(options = {}) {
954
- const topK = optionalPositiveInteger(options.top_k ?? options.topK, 'top_k') ?? 5;
955
- const datasetRows = [];
956
- if (options.locomoData !== undefined) {
957
- datasetRows.push(scoreDataset(parseLocomoDataset(options.locomoData, { max_qa: options.max_locomo_qa ?? options.maxLocomoQa }), topK));
958
- }
959
- if (options.longMemEvalData !== undefined || options.longmemevalData !== undefined) {
960
- datasetRows.push(scoreDataset(parseLongMemEvalDataset(options.longMemEvalData ?? options.longmemevalData, { max_items: options.max_longmemeval_items ?? options.maxLongMemEvalItems }), topK));
961
- }
962
- if (datasetRows.length === 0) throw new Error('At least one standard dataset is required: provide locomoData and/or longMemEvalData');
963
- return buildSuiteReport(datasetRows, topK, options);
964
- }
965
-
966
- export async function runStandardMemoryBenchmarkSuiteFromFiles(options = {}) {
967
- const datasetRows = [];
968
- const topK = optionalPositiveInteger(options.top_k ?? options.topK, 'top_k') ?? 5;
969
- if (options.locomo !== undefined || options.locomoPath !== undefined) {
970
- const path = options.locomo ?? options.locomoPath;
971
- const loaded = await readJsonWithSha256(path);
972
- datasetRows.push({
973
- ...scoreDataset(parseLocomoDataset(loaded.data, { max_qa: options.max_locomo_qa ?? options.maxLocomoQa }), topK),
974
- local_file_name: publicFileName(path),
975
- input_sha256: loaded.sha256,
976
- });
977
- }
978
- if (options.longmemeval !== undefined || options.longmemevalPath !== undefined || options.longMemEvalPath !== undefined) {
979
- const path = options.longmemeval ?? options.longmemevalPath ?? options.longMemEvalPath;
980
- const maxItems = optionalPositiveInteger(options.max_longmemeval_items ?? options.maxLongMemEvalItems, 'max_longmemeval_items');
981
- const loaded = await readLongMemEvalJsonWithSha256(path, maxItems);
982
- datasetRows.push({
983
- ...scoreDataset(parseLongMemEvalDataset(loaded.data, { max_items: maxItems }), topK),
984
- local_file_name: publicFileName(path),
985
- input_sha256: loaded.sha256,
986
- });
987
- }
988
- if (datasetRows.length === 0) throw new Error('Provide --locomo <path> and/or --longmemeval <path>');
989
- return buildSuiteReport(datasetRows, topK, options);
990
- }
991
-
992
- function buildSuiteReport(datasetRows, topK, options) {
993
- return {
994
- schema: STANDARD_MEMORY_BENCHMARK_SUITE_SCHEMA,
995
- generated_at: options.generated_at ?? new Date().toISOString(),
996
- package: {
997
- name: 'enigma-memory',
998
- version: '0.1.13',
999
- },
1000
- public_safe: true,
1001
- top_k: topK,
1002
- source_urls: {
1003
- locomo: LOCOMO_SOURCE_URL,
1004
- longmemeval: LONGMEMEVAL_SOURCE_URLS,
1005
- },
1006
- license_and_boundary_notes: [
1007
- 'LoCoMo source data is CC BY-NC 4.0; keep local dataset files and raw conversations out of public reports unless separately reviewed.',
1008
- 'LongMemEval cleaned files are operator-supplied local JSON files from the upstream Hugging Face dataset repository.',
1009
- 'Scores are retrieval/evidence proxy metrics over official dataset labels, not LLM-generated answer accuracy.',
1010
- 'No provider APIs, hosted runtimes, competitor SDKs, or external accounts are called by this runner.',
1011
- 'Rows are local deterministic methods only; no third-party competitor scores or benchmark-leadership claims are emitted.',
1012
- ],
1013
- benchmark_boundaries: {
1014
- official_dataset_files_required: true,
1015
- credentials_required: false,
1016
- external_provider_calls: false,
1017
- llm_answer_accuracy_scored: false,
1018
- retrieval_evidence_proxy_scored: true,
1019
- raw_question_text_included: false,
1020
- raw_answer_text_included: false,
1021
- raw_conversation_text_included: false,
1022
- provider_deletion_claim: false,
1023
- model_forgetting_claim: false,
1024
- roi_or_provider_invoice_savings_claim: false,
1025
- compliance_certification_claim: false,
1026
- benchmark_leadership_claim: false,
1027
- },
1028
- relevance_logic: {
1029
- token_extraction: 'keyword_filter uses basic lowercase /[a-z0-9]+(?:[-_][a-z0-9]+)*/ overlap after stopword removal; enigma_relevance additionally applies deterministic suffix stemming for ing, ed, and plural s forms',
1030
- production_alignment: 'public-safe local approximation of production query-aware retrieval using query/content stems, role/session/kind/tag hints, category/task hints, temporal/date hints, and phrase/proximity boosts; no private memory is emitted',
1031
- keyword_filter_fallback: 'empty result when no query/content token overlap exists',
1032
- enigma_relevance_fallback: 'falls back to all local candidates only when no enhanced relevance signal exists, then applies deterministic local ranking and --top-k',
1033
- provider_api_used: false,
1034
- llm_used: false,
1035
- },
1036
- local_methods: STANDARD_MEMORY_BENCHMARK_METHODS.map((method) => ({ ...method })),
1037
- datasets: datasetRows,
1038
- dataset_rows: datasetRows,
1039
- };
1040
- }
1041
-
1042
- function usage() {
1043
- return `Usage: node scripts/run-standard-memory-benchmarks.mjs [--locomo <path>] [--longmemeval <path>] [--max-locomo-qa <n>] [--max-longmemeval-items <n>] [--top-k <n>] [--out <path>]\n\nProduces schema ${STANDARD_MEMORY_BENCHMARK_SUITE_SCHEMA}. Raw question, answer, and conversation text are never written to the report. With --longmemeval and --max-longmemeval-items, the local top-level JSON array is streamed for hashing and only the requested sample items are parsed.`;
1044
- }
1045
-
1046
- async function main() {
1047
- const options = parseArgs();
1048
- if (options.help) {
1049
- console.log(usage());
1050
- return;
1051
- }
1052
- const report = await runStandardMemoryBenchmarkSuiteFromFiles(options);
1053
- const serialized = `${JSON.stringify(report, null, 2)}\n`;
1054
- if (options.out) {
1055
- const outPath = resolve(options.out);
1056
- await mkdir(dirname(outPath), { recursive: true });
1057
- await writeFile(outPath, serialized);
1058
- } else {
1059
- process.stdout.write(serialized);
1060
- }
1061
- }
1062
-
1063
- const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
1064
- const modulePath = fileURLToPath(import.meta.url);
1065
- if (invokedPath === modulePath) {
1066
- main().catch((error) => {
1067
- console.error(error instanceof Error ? error.message : String(error));
1068
- process.exitCode = 1;
1069
- });
1070
- }
1
+ #!/usr/bin/env node
2
+ import { createReadStream } from 'node:fs';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { basename, dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { createHash } from 'node:crypto';
7
+ import { performance } from 'node:perf_hooks';
8
+ import { StringDecoder } from 'node:string_decoder';
9
+ import { estimateTextTokens } from '../packages/optimizer/src/index.js';
10
+
11
+ export const STANDARD_MEMORY_BENCHMARK_SUITE_SCHEMA = 'enigma.standard_memory_benchmark_suite.v1';
12
+
13
+ export const STANDARD_MEMORY_BENCHMARK_METHODS = Object.freeze([
14
+ Object.freeze({
15
+ id: 'full_context',
16
+ label: 'Full context',
17
+ boundary: 'Supplies every parsed memory record for each query; no provider API or model answer generation.',
18
+ uses_top_k: false,
19
+ }),
20
+ Object.freeze({
21
+ id: 'recency_last_n',
22
+ label: 'Recency last N',
23
+ boundary: 'Supplies the most recent local memory records up to --top-k.',
24
+ uses_top_k: true,
25
+ }),
26
+ Object.freeze({
27
+ id: 'keyword_filter',
28
+ label: 'Keyword filter',
29
+ boundary: 'Supplies local memory records whose public-safe deterministic tokens overlap the query, capped by --top-k.',
30
+ uses_top_k: true,
31
+ }),
32
+ Object.freeze({
33
+ id: 'enigma_relevance',
34
+ label: 'Enigma relevance',
35
+ boundary: 'Uses deterministic query-aware relevance features over local public-safe memory metadata and content tokens, then ranks locally without provider APIs.',
36
+ uses_top_k: true,
37
+ }),
38
+ ]);
39
+
40
+ const LOCOMO_SOURCE_URL = 'https://raw.githubusercontent.com/snap-research/locomo/main/data/locomo10.json';
41
+ const LONGMEMEVAL_SOURCE_URLS = Object.freeze([
42
+ 'https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_oracle.json',
43
+ 'https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json',
44
+ 'https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_m_cleaned.json',
45
+ ]);
46
+
47
+ const QUERY_RELEVANCE_STOPWORDS = new Set([
48
+ 'about',
49
+ 'after',
50
+ 'again',
51
+ 'against',
52
+ 'also',
53
+ 'and',
54
+ 'any',
55
+ 'are',
56
+ 'assistant',
57
+ 'because',
58
+ 'been',
59
+ 'before',
60
+ 'being',
61
+ 'between',
62
+ 'can',
63
+ 'could',
64
+ 'current',
65
+ 'does',
66
+ 'from',
67
+ 'has',
68
+ 'have',
69
+ 'how',
70
+ 'into',
71
+ 'its',
72
+ 'latest',
73
+ 'more',
74
+ 'most',
75
+ 'number',
76
+ 'own',
77
+ 'owns',
78
+ 'please',
79
+ 'should',
80
+ 'that',
81
+ 'the',
82
+ 'their',
83
+ 'then',
84
+ 'there',
85
+ 'these',
86
+ 'they',
87
+ 'this',
88
+ 'use',
89
+ 'using',
90
+ 'was',
91
+ 'what',
92
+ 'when',
93
+ 'where',
94
+ 'which',
95
+ 'who',
96
+ 'whose',
97
+ 'why',
98
+ 'with',
99
+ 'would',
100
+ ]);
101
+
102
+ function parseArgs(argv = process.argv.slice(2)) {
103
+ const options = { top_k: 5 };
104
+ for (let index = 0; index < argv.length; index += 1) {
105
+ const arg = argv[index];
106
+ if (arg === '--locomo') {
107
+ options.locomo = requiredFlagValue(argv, index, arg);
108
+ index += 1;
109
+ } else if (arg === '--longmemeval') {
110
+ options.longmemeval = requiredFlagValue(argv, index, arg);
111
+ index += 1;
112
+ } else if (arg === '--max-locomo-qa') {
113
+ options.max_locomo_qa = positiveInteger(requiredFlagValue(argv, index, arg), arg);
114
+ index += 1;
115
+ } else if (arg === '--max-longmemeval-items') {
116
+ options.max_longmemeval_items = positiveInteger(requiredFlagValue(argv, index, arg), arg);
117
+ index += 1;
118
+ } else if (arg === '--top-k') {
119
+ options.top_k = positiveInteger(requiredFlagValue(argv, index, arg), arg);
120
+ index += 1;
121
+ } else if (arg === '--out') {
122
+ options.out = requiredFlagValue(argv, index, arg);
123
+ index += 1;
124
+ } else if (arg === '--help' || arg === '-h') {
125
+ options.help = true;
126
+ } else {
127
+ throw new Error(`Unknown option ${arg}`);
128
+ }
129
+ }
130
+ return options;
131
+ }
132
+
133
+ function requiredFlagValue(argv, index, flag) {
134
+ const value = argv[index + 1];
135
+ if (value === undefined || value.startsWith('--')) throw new Error(`${flag} requires a value`);
136
+ return value;
137
+ }
138
+
139
+ function positiveInteger(value, name) {
140
+ const number = Number(value);
141
+ if (!Number.isInteger(number) || number <= 0) throw new Error(`${name} must be a positive integer`);
142
+ return number;
143
+ }
144
+
145
+ function optionalPositiveInteger(value, name) {
146
+ if (value === undefined || value === null) return undefined;
147
+ return positiveInteger(value, name);
148
+ }
149
+
150
+ function publicFileName(path) {
151
+ return path === undefined || path === null ? undefined : basename(String(path));
152
+ }
153
+
154
+ async function readJsonWithSha256(path) {
155
+ const raw = await readFile(path, 'utf8');
156
+ return {
157
+ data: JSON.parse(raw),
158
+ sha256: createHash('sha256').update(raw).digest('hex'),
159
+ };
160
+ }
161
+
162
+ function isJsonWhitespace(char) {
163
+ const code = char.charCodeAt(0);
164
+ return code === 0x20 || code === 0x0a || code === 0x0d || code === 0x09 || code === 0xfeff;
165
+ }
166
+
167
+ function createTopLevelArraySampler(maxItems, name) {
168
+ const items = [];
169
+ let started = false;
170
+ let closed = false;
171
+ let collecting = false;
172
+ let doneCollecting = false;
173
+ let expectSeparator = false;
174
+ let depth = 0;
175
+ let inString = false;
176
+ let escaped = false;
177
+ let current = '';
178
+
179
+ function fail(message) {
180
+ throw new Error(`${name} sample-mode JSON parse failed: ${message}`);
181
+ }
182
+
183
+ function finishItem() {
184
+ items.push(current);
185
+ current = '';
186
+ collecting = false;
187
+ expectSeparator = true;
188
+ if (items.length >= maxItems) doneCollecting = true;
189
+ }
190
+
191
+ return {
192
+ write(text) {
193
+ for (const char of text) {
194
+ if (!started) {
195
+ if (isJsonWhitespace(char)) continue;
196
+ if (char !== '[') fail('expected a top-level array');
197
+ started = true;
198
+ continue;
199
+ }
200
+
201
+ if (doneCollecting) continue;
202
+
203
+ if (closed) {
204
+ if (!isJsonWhitespace(char)) fail('found trailing data after the top-level array');
205
+ continue;
206
+ }
207
+
208
+ if (!collecting) {
209
+ if (isJsonWhitespace(char)) continue;
210
+ if (expectSeparator) {
211
+ if (char === ',') {
212
+ expectSeparator = false;
213
+ continue;
214
+ }
215
+ if (char === ']') {
216
+ closed = true;
217
+ continue;
218
+ }
219
+ fail('expected a comma or closing bracket between items');
220
+ }
221
+ if (char === ']') {
222
+ closed = true;
223
+ continue;
224
+ }
225
+ if (char !== '{' && char !== '[') fail('expected each sampled item to be an object or array');
226
+ collecting = true;
227
+ current = char;
228
+ depth = 1;
229
+ continue;
230
+ }
231
+
232
+ current += char;
233
+ if (inString) {
234
+ if (escaped) {
235
+ escaped = false;
236
+ } else if (char === '\\') {
237
+ escaped = true;
238
+ } else if (char === '"') {
239
+ inString = false;
240
+ }
241
+ continue;
242
+ }
243
+ if (char === '"') {
244
+ inString = true;
245
+ } else if (char === '{' || char === '[') {
246
+ depth += 1;
247
+ } else if (char === '}' || char === ']') {
248
+ depth -= 1;
249
+ if (depth < 0) fail('encountered an unmatched closing bracket');
250
+ if (depth === 0) finishItem();
251
+ }
252
+ }
253
+ },
254
+ get done() {
255
+ return doneCollecting;
256
+ },
257
+ finish() {
258
+ if (!started) fail('empty input');
259
+ if (!doneCollecting && collecting) fail('ended inside a sampled item');
260
+ if (!doneCollecting && inString) fail('ended inside a string');
261
+ if (!doneCollecting && !closed) fail('ended before the top-level array closed');
262
+ return JSON.parse(`[${items.join(',')}]`);
263
+ },
264
+ };
265
+ }
266
+
267
+ async function readJsonArraySampleWithSha256(path, maxItems, name) {
268
+ const hash = createHash('sha256');
269
+ const decoder = new StringDecoder('utf8');
270
+ const sampler = createTopLevelArraySampler(maxItems, name);
271
+ for await (const chunk of createReadStream(path)) {
272
+ hash.update(chunk);
273
+ if (!sampler.done) sampler.write(decoder.write(chunk));
274
+ }
275
+ if (!sampler.done) {
276
+ const tail = decoder.end();
277
+ if (tail.length > 0) sampler.write(tail);
278
+ }
279
+ return {
280
+ data: sampler.finish(),
281
+ sha256: hash.digest('hex'),
282
+ };
283
+ }
284
+
285
+ async function readLongMemEvalJsonWithSha256(path, maxItems) {
286
+ if (maxItems === undefined) return readJsonWithSha256(path);
287
+ try {
288
+ return await readJsonArraySampleWithSha256(path, maxItems, 'LongMemEval');
289
+ } catch (error) {
290
+ if (error instanceof Error && error.message === 'LongMemEval sample-mode JSON parse failed: expected a top-level array') {
291
+ return readJsonWithSha256(path);
292
+ }
293
+ throw error;
294
+ }
295
+ }
296
+
297
+ function normalizeDatasetArray(data, name) {
298
+ if (Array.isArray(data)) return data;
299
+ if (data && typeof data === 'object') {
300
+ for (const key of ['data', 'items', 'examples', 'samples']) {
301
+ if (Array.isArray(data[key])) return data[key];
302
+ }
303
+ }
304
+ throw new TypeError(`${name} dataset must be a JSON array or object containing an array`);
305
+ }
306
+
307
+ function addMeaningfulToken(tokens, token) {
308
+ if (token.length < 3) return;
309
+ if (!/[a-z]/u.test(token)) return;
310
+ if (QUERY_RELEVANCE_STOPWORDS.has(token)) return;
311
+ tokens.add(token);
312
+ }
313
+
314
+ function stemToken(token) {
315
+ if (token.length > 5 && token.endsWith('ing')) {
316
+ let stem = token.slice(0, -3);
317
+ if (stem.length > 3 && stem.at(-1) === stem.at(-2)) stem = stem.slice(0, -1);
318
+ return stem;
319
+ }
320
+ if (token.length > 4 && token.endsWith('ed')) {
321
+ let stem = token.slice(0, -2);
322
+ if (stem.length > 3 && stem.at(-1) === stem.at(-2)) stem = stem.slice(0, -1);
323
+ return stem;
324
+ }
325
+ if (token.length > 4 && token.endsWith('ies')) return `${token.slice(0, -3)}y`;
326
+ if (token.length > 4 && token.endsWith('es')) return token.slice(0, -2);
327
+ if (token.length > 3 && token.endsWith('s') && !token.endsWith('ss')) return token.slice(0, -1);
328
+ return token;
329
+ }
330
+
331
+ function addStemmedMeaningfulToken(tokens, token) {
332
+ addMeaningfulToken(tokens, token);
333
+ const stem = stemToken(token);
334
+ addMeaningfulToken(tokens, stem);
335
+ if (token.endsWith('ed') || token.endsWith('ing')) addMeaningfulToken(tokens, `${stem}e`);
336
+ }
337
+
338
+ function meaningfulTokensFrom(value) {
339
+ const tokens = new Set();
340
+ if (value === undefined || value === null) return tokens;
341
+ for (const match of String(value).toLowerCase().matchAll(/[a-z0-9]+(?:[-_][a-z0-9]+)*/gu)) {
342
+ const token = match[0];
343
+ addMeaningfulToken(tokens, token);
344
+ if (token.includes('-') || token.includes('_')) {
345
+ for (const part of token.split(/[-_]+/u)) addMeaningfulToken(tokens, part);
346
+ }
347
+ }
348
+ return tokens;
349
+ }
350
+
351
+ function stemmedMeaningfulTokensFrom(value) {
352
+ const tokens = new Set();
353
+ if (value === undefined || value === null) return tokens;
354
+ for (const match of String(value).toLowerCase().matchAll(/[a-z0-9]+(?:[-_][a-z0-9]+)*/gu)) {
355
+ const token = match[0];
356
+ addStemmedMeaningfulToken(tokens, token);
357
+ if (token.includes('-') || token.includes('_')) {
358
+ for (const part of token.split(/[-_]+/u)) addStemmedMeaningfulToken(tokens, part);
359
+ }
360
+ }
361
+ return tokens;
362
+ }
363
+
364
+ function stemmedTokenSequenceFrom(value) {
365
+ const sequence = [];
366
+ if (value === undefined || value === null) return sequence;
367
+ for (const match of String(value).toLowerCase().matchAll(/[a-z0-9]+(?:[-_][a-z0-9]+)*/gu)) {
368
+ const token = match[0];
369
+ const parts = token.includes('-') || token.includes('_') ? token.split(/[-_]+/u) : [token];
370
+ for (const part of parts) {
371
+ const stem = stemToken(part);
372
+ if (stem.length >= 3 && /[a-z]/u.test(stem) && !QUERY_RELEVANCE_STOPWORDS.has(stem)) sequence.push(stem);
373
+ }
374
+ }
375
+ return sequence;
376
+ }
377
+
378
+ function tokenOverlapScore(queryTokens, record) {
379
+ if (queryTokens.size === 0) return 0;
380
+ const recordTokens = meaningfulTokensFrom(record.content);
381
+ let score = 0;
382
+ for (const token of queryTokens) if (recordTokens.has(token)) score += 1;
383
+ return score;
384
+ }
385
+
386
+ function normalizedTagSessionToken(value) {
387
+ return String(value ?? '').toLowerCase().replace(/^session[-_:]?/u, '').replace(/[^a-z0-9]+/gu, '');
388
+ }
389
+
390
+ function roleHintsFrom(value) {
391
+ const hints = new Set();
392
+ const text = String(value ?? '').toLowerCase();
393
+ for (const match of text.matchAll(/\b(?:assistant|user|system|human|agent|speaker[-_\s]?[a-z0-9]+)\b/gu)) {
394
+ const compact = match[0].replace(/\s+/gu, '_');
395
+ hints.add(compact);
396
+ for (const token of stemmedMeaningfulTokensFrom(compact)) hints.add(token);
397
+ }
398
+ return hints;
399
+ }
400
+
401
+ function sessionHintsFrom(value) {
402
+ const hints = new Set();
403
+ const text = String(value ?? '').toLowerCase();
404
+ for (const match of text.matchAll(/\b(?:session|sess)\s*[-_:]?\s*([a-z0-9]+)\b/gu)) hints.add(match[1]);
405
+ for (const match of text.matchAll(/\bd\s*[-_:]?\s*(\d+)\b/gu)) hints.add(`d${Number(match[1])}`);
406
+ for (const match of text.matchAll(/\bsession[-_]([a-z0-9]+)\b/gu)) hints.add(match[1]);
407
+ return hints;
408
+ }
409
+
410
+ function dateHintsFrom(value) {
411
+ const hints = new Set();
412
+ const text = String(value ?? '').toLowerCase();
413
+ for (const match of text.matchAll(/\b(?:19|20)\d{2}\b/gu)) hints.add(match[0]);
414
+ for (const match of text.matchAll(/\b\d{4}[-/]\d{1,2}(?:[-/]\d{1,2})?\b/gu)) hints.add(match[0].replace(/\D+/gu, '-'));
415
+ for (const match of text.matchAll(/\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b/gu)) hints.add(match[0].slice(0, 3));
416
+ for (const match of text.matchAll(/\b(?:today|yesterday|tomorrow|recent|recently|latest|newest|current|previous|last|earliest|oldest)\b/gu)) hints.add(match[0]);
417
+ return hints;
418
+ }
419
+
420
+ function recordSessionTokens(record) {
421
+ const tokens = new Set();
422
+ const values = [record.session_id, record.dialog_id, record.turn_id, ...(record.tags ?? [])];
423
+ for (const value of values) {
424
+ if (value === undefined || value === null) continue;
425
+ const normalized = normalizedTagSessionToken(value);
426
+ if (normalized) tokens.add(normalized);
427
+ for (const hint of sessionHintsFrom(value)) tokens.add(normalizedTagSessionToken(hint));
428
+ }
429
+ return tokens;
430
+ }
431
+
432
+ function countSetIntersection(left, right) {
433
+ let count = 0;
434
+ for (const value of left) if (right.has(value)) count += 1;
435
+ return count;
436
+ }
437
+
438
+ function phraseAndProximityScore(querySequence, recordSequence) {
439
+ if (querySequence.length < 2 || recordSequence.length < 2) return 0;
440
+ const recordBigrams = new Set();
441
+ const positions = new Map();
442
+ for (let index = 0; index < recordSequence.length; index += 1) {
443
+ const token = recordSequence[index];
444
+ if (!positions.has(token)) positions.set(token, []);
445
+ positions.get(token).push(index);
446
+ if (index > 0) recordBigrams.add(`${recordSequence[index - 1]}\u0000${token}`);
447
+ }
448
+ let score = 0;
449
+ for (let index = 1; index < querySequence.length; index += 1) {
450
+ const previous = querySequence[index - 1];
451
+ const current = querySequence[index];
452
+ if (previous === current) continue;
453
+ if (recordBigrams.has(`${previous}\u0000${current}`)) {
454
+ score += 10;
455
+ continue;
456
+ }
457
+ const leftPositions = positions.get(previous);
458
+ const rightPositions = positions.get(current);
459
+ if (!leftPositions || !rightPositions) continue;
460
+ let near = false;
461
+ for (const left of leftPositions) {
462
+ for (const right of rightPositions) {
463
+ if (Math.abs(left - right) <= 6) {
464
+ near = true;
465
+ break;
466
+ }
467
+ }
468
+ if (near) break;
469
+ }
470
+ if (near) score += 4;
471
+ }
472
+ return score;
473
+ }
474
+
475
+ function hasRecencyIntent(hints) {
476
+ for (const hint of hints) {
477
+ if (hint === 'recent' || hint === 'recently' || hint === 'latest' || hint === 'newest' || hint === 'current' || hint === 'previous' || hint === 'last') return true;
478
+ }
479
+ return false;
480
+ }
481
+
482
+ function enigmaRelevanceScore(query, record) {
483
+ const question = String(query.question ?? '');
484
+ const queryTokens = stemmedMeaningfulTokensFrom(question);
485
+ const categoryTokens = stemmedMeaningfulTokensFrom(`${query.category ?? ''} ${query.question_type ?? ''}`);
486
+ const contentTokens = stemmedMeaningfulTokensFrom(record.content);
487
+ const metadataTokens = stemmedMeaningfulTokensFrom(`${record.kind ?? ''} ${(record.tags ?? []).join(' ')}`);
488
+ for (const token of stemmedMeaningfulTokensFrom(`${record.role ?? ''} ${record.session_id ?? ''} ${record.dialog_id ?? ''} ${record.turn_id ?? ''}`)) {
489
+ metadataTokens.add(token);
490
+ }
491
+
492
+ const contentMatches = countSetIntersection(queryTokens, contentTokens);
493
+ const metadataMatches = countSetIntersection(queryTokens, metadataTokens);
494
+ const categoryMatches = countSetIntersection(categoryTokens, metadataTokens) + countSetIntersection(categoryTokens, contentTokens);
495
+ const roleMatches = countSetIntersection(roleHintsFrom(question), roleHintsFrom(record.role));
496
+ const querySessionHints = sessionHintsFrom(question);
497
+ const recordSessions = recordSessionTokens(record);
498
+ const sessionMatches = countSetIntersection(querySessionHints, recordSessions);
499
+ const queryDateHints = dateHintsFrom(question);
500
+ const temporalMatches = countSetIntersection(queryDateHints, dateHintsFrom(`${record.content} ${(record.tags ?? []).join(' ')}`));
501
+ const temporalRecencyScore = hasRecencyIntent(queryDateHints) ? Math.min(6, Math.log2(record.ordinal + 2)) : 0;
502
+ const phraseScore = phraseAndProximityScore(stemmedTokenSequenceFrom(question), stemmedTokenSequenceFrom(record.content));
503
+
504
+ return {
505
+ record,
506
+ score: (contentMatches * 12)
507
+ + (metadataMatches * 4)
508
+ + (categoryMatches * 3)
509
+ + (roleMatches * 18)
510
+ + (sessionMatches * 16)
511
+ + (temporalMatches * 8)
512
+ + temporalRecencyScore
513
+ + phraseScore,
514
+ contentMatches,
515
+ metadataMatches,
516
+ categoryMatches,
517
+ roleMatches,
518
+ sessionMatches,
519
+ temporalMatches,
520
+ temporalRecencyScore,
521
+ phraseScore,
522
+ };
523
+ }
524
+
525
+ function compareRecordId(left, right) {
526
+ return String(left.id).localeCompare(String(right.id));
527
+ }
528
+
529
+ function compareRecencyDesc(left, right) {
530
+ if (left.ordinal !== right.ordinal) return right.ordinal - left.ordinal;
531
+ return compareRecordId(left, right);
532
+ }
533
+
534
+ function rankedByOverlap(records, query) {
535
+ const queryTokens = meaningfulTokensFrom(query);
536
+ if (queryTokens.size === 0) return [];
537
+ const scored = [];
538
+ for (const record of records) {
539
+ const score = tokenOverlapScore(queryTokens, record);
540
+ if (score > 0) scored.push({ record, score });
541
+ }
542
+ scored.sort((left, right) => {
543
+ if (left.score !== right.score) return right.score - left.score;
544
+ return compareRecencyDesc(left.record, right.record);
545
+ });
546
+ return scored.map((item) => item.record);
547
+ }
548
+
549
+ function rankedByEnigmaRelevance(records, query) {
550
+ const scored = [];
551
+ for (const record of records) {
552
+ const item = enigmaRelevanceScore(query, record);
553
+ if (item.score > 0) scored.push(item);
554
+ }
555
+ if (scored.length === 0) return [...records].sort(compareRecencyDesc);
556
+ scored.sort((left, right) => {
557
+ if (left.score !== right.score) return right.score - left.score;
558
+ if (left.phraseScore !== right.phraseScore) return right.phraseScore - left.phraseScore;
559
+ if (left.contentMatches !== right.contentMatches) return right.contentMatches - left.contentMatches;
560
+ if (left.roleMatches !== right.roleMatches) return right.roleMatches - left.roleMatches;
561
+ if (left.sessionMatches !== right.sessionMatches) return right.sessionMatches - left.sessionMatches;
562
+ if (left.temporalMatches !== right.temporalMatches) return right.temporalMatches - left.temporalMatches;
563
+ if (left.temporalRecencyScore !== right.temporalRecencyScore) return right.temporalRecencyScore - left.temporalRecencyScore;
564
+ if (left.metadataMatches !== right.metadataMatches) return right.metadataMatches - left.metadataMatches;
565
+ return compareRecencyDesc(left.record, right.record);
566
+ });
567
+ return scored.map((item) => item.record);
568
+ }
569
+
570
+ function selectRecords(methodId, records, query, topK) {
571
+ if (methodId === 'full_context') return records;
572
+ if (methodId === 'recency_last_n') return [...records].sort(compareRecencyDesc).slice(0, topK);
573
+ if (methodId === 'keyword_filter') return rankedByOverlap(records, query.question).slice(0, topK);
574
+ if (methodId === 'enigma_relevance') return rankedByEnigmaRelevance(records, query).slice(0, topK);
575
+ throw new Error(`Unknown method ${methodId}`);
576
+ }
577
+
578
+ function estimatePromptTokens(question, selectedRecords) {
579
+ let tokens = estimateTextTokens(question);
580
+ for (const record of selectedRecords) tokens += record.estimated_tokens;
581
+ return tokens;
582
+ }
583
+
584
+ function percentile(values, ratio) {
585
+ if (values.length === 0) return 0;
586
+ const sorted = [...values].sort((left, right) => left - right);
587
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1));
588
+ return sorted[index];
589
+ }
590
+
591
+ function latencySummary(values) {
592
+ return {
593
+ samples: values.length,
594
+ p50_ms: round6(percentile(values, 0.5)),
595
+ p95_ms: round6(percentile(values, 0.95)),
596
+ min_ms: round6(values.length === 0 ? 0 : Math.min(...values)),
597
+ max_ms: round6(values.length === 0 ? 0 : Math.max(...values)),
598
+ };
599
+ }
600
+
601
+ function rate(numerator, denominator) {
602
+ if (denominator === 0) return null;
603
+ return round6(numerator / denominator);
604
+ }
605
+
606
+ function round6(value) {
607
+ return Number(value.toFixed(6));
608
+ }
609
+
610
+ function mean(total, count) {
611
+ return count === 0 ? 0 : round6(total / count);
612
+ }
613
+
614
+ function recordFromContent(args) {
615
+ const content = String(args.content ?? '');
616
+ return {
617
+ id: args.id,
618
+ dataset_item_id: args.dataset_item_id,
619
+ session_id: args.session_id,
620
+ turn_id: args.turn_id,
621
+ dialog_id: args.dialog_id,
622
+ role: args.role,
623
+ kind: args.kind,
624
+ tags: args.tags ?? [],
625
+ has_answer: args.has_answer === true,
626
+ ordinal: args.ordinal,
627
+ content,
628
+ estimated_tokens: estimateTextTokens(content),
629
+ };
630
+ }
631
+
632
+ function parseLocomoEvidenceLabels(evidence) {
633
+ const labels = new Set();
634
+ const stack = Array.isArray(evidence) ? [...evidence] : [evidence];
635
+ while (stack.length > 0) {
636
+ const value = stack.shift();
637
+ if (Array.isArray(value)) {
638
+ stack.push(...value);
639
+ continue;
640
+ }
641
+ if (value === undefined || value === null) continue;
642
+ for (const match of String(value).matchAll(/D\s*(\d+)\s*:\s*(\d+)/giu)) {
643
+ labels.add(`D${Number(match[1])}:${Number(match[2])}`);
644
+ }
645
+ }
646
+ return labels;
647
+ }
648
+
649
+ function dialogIdForTurn(sessionNumber, turn, turnIndex) {
650
+ const raw = turn?.dia_id ?? turn?.dialog_id ?? turn?.turn_id ?? turn?.id ?? turnIndex + 1;
651
+ const text = String(raw);
652
+ const match = text.match(/^D\s*(\d+)\s*:\s*(\d+)$/iu);
653
+ if (match) return `D${Number(match[1])}:${Number(match[2])}`;
654
+ const numeric = text.match(/\d+/u)?.[0] ?? String(turnIndex + 1);
655
+ return `D${sessionNumber}:${Number(numeric)}`;
656
+ }
657
+
658
+ export function parseLocomoDataset(data, options = {}) {
659
+ const maxQa = optionalPositiveInteger(options.max_qa ?? options.maxQa, 'max_locomo_qa');
660
+ const rows = normalizeDatasetArray(data, 'LoCoMo');
661
+ const records = [];
662
+ const queries = [];
663
+ let ordinal = 0;
664
+ for (let sampleIndex = 0; sampleIndex < rows.length; sampleIndex += 1) {
665
+ const sample = rows[sampleIndex] ?? {};
666
+ const itemId = String(sample.sample_id ?? sample.id ?? `sample_${sampleIndex + 1}`);
667
+ const conversation = sample.conversation ?? {};
668
+ const sessionNames = Object.keys(conversation)
669
+ .map((key) => {
670
+ const match = key.match(/^session_(\d+)$/u);
671
+ return match ? { key, sessionNumber: Number(match[1]) } : null;
672
+ })
673
+ .filter(Boolean)
674
+ .sort((left, right) => left.sessionNumber - right.sessionNumber);
675
+ for (const { key, sessionNumber } of sessionNames) {
676
+ const session = conversation[key];
677
+ if (!Array.isArray(session)) continue;
678
+ for (let turnIndex = 0; turnIndex < session.length; turnIndex += 1) {
679
+ const turn = session[turnIndex] ?? {};
680
+ const dialogId = dialogIdForTurn(sessionNumber, turn, turnIndex);
681
+ records.push(recordFromContent({
682
+ id: `locomo:${itemId}:${dialogId}`,
683
+ dataset_item_id: itemId,
684
+ session_id: `D${sessionNumber}`,
685
+ turn_id: dialogId,
686
+ dialog_id: dialogId,
687
+ role: turn.speaker ?? turn.role ?? undefined,
688
+ kind: 'locomo_conversation_turn',
689
+ tags: ['locomo', `session_${sessionNumber}`],
690
+ ordinal,
691
+ content: turn.text ?? turn.content ?? turn.message ?? '',
692
+ }));
693
+ ordinal += 1;
694
+ }
695
+ }
696
+ const qaRows = Array.isArray(sample.qa) ? sample.qa : [];
697
+ for (let qaIndex = 0; qaIndex < qaRows.length; qaIndex += 1) {
698
+ if (maxQa !== undefined && queries.length >= maxQa) break;
699
+ const qa = qaRows[qaIndex] ?? {};
700
+ const evidenceDialogIds = parseLocomoEvidenceLabels(qa.evidence);
701
+ queries.push({
702
+ id: `locomo:${itemId}:qa_${qaIndex + 1}`,
703
+ dataset_item_id: itemId,
704
+ question: String(qa.question ?? ''),
705
+ category: qa.category === undefined ? undefined : String(qa.category),
706
+ evidence_dialog_ids: evidenceDialogIds,
707
+ abstention: evidenceDialogIds.size === 0,
708
+ });
709
+ }
710
+ if (maxQa !== undefined && queries.length >= maxQa) break;
711
+ }
712
+ return {
713
+ id: 'locomo',
714
+ label: 'LoCoMo',
715
+ source_url: LOCOMO_SOURCE_URL,
716
+ license: 'CC BY-NC 4.0',
717
+ parser: 'conversation session turns as memory records; qa evidence labels mapped to dialog ids such as D1:3 and semicolon-separated labels',
718
+ task_categories: ['multi-session QA', 'event summarization', 'multimodal generation over long conversations'],
719
+ records,
720
+ queries,
721
+ };
722
+ }
723
+
724
+ function sessionIdAt(ids, index) {
725
+ if (Array.isArray(ids) && ids[index] !== undefined && ids[index] !== null) return String(ids[index]);
726
+ return String(index);
727
+ }
728
+
729
+ function answerSessionIds(item) {
730
+ if (!Array.isArray(item.answer_session_ids)) return new Set();
731
+ return new Set(item.answer_session_ids.map((id) => String(id)));
732
+ }
733
+
734
+ function isLongMemEvalAbstention(item, answerSessions) {
735
+ const id = String(item.question_id ?? item.id ?? '');
736
+ if (id.endsWith('_abs') || id.includes('_abs_')) return true;
737
+ if (String(item.question_type ?? '').toLowerCase().includes('abst')) return true;
738
+ return answerSessions.size === 0;
739
+ }
740
+
741
+ export function parseLongMemEvalDataset(data, options = {}) {
742
+ const maxItems = optionalPositiveInteger(options.max_items ?? options.maxItems, 'max_longmemeval_items');
743
+ const rows = normalizeDatasetArray(data, 'LongMemEval');
744
+ const records = [];
745
+ const queries = [];
746
+ let ordinal = 0;
747
+ const limit = maxItems === undefined ? rows.length : Math.min(rows.length, maxItems);
748
+ for (let itemIndex = 0; itemIndex < limit; itemIndex += 1) {
749
+ const item = rows[itemIndex] ?? {};
750
+ const itemId = String(item.question_id ?? item.id ?? `item_${itemIndex + 1}`);
751
+ const sessions = Array.isArray(item.haystack_sessions) ? item.haystack_sessions : [];
752
+ const sessionIds = item.haystack_session_ids;
753
+ const evidenceTurnIds = new Set();
754
+ for (let sessionIndex = 0; sessionIndex < sessions.length; sessionIndex += 1) {
755
+ const session = sessions[sessionIndex];
756
+ if (!Array.isArray(session)) continue;
757
+ const sessionId = sessionIdAt(sessionIds, sessionIndex);
758
+ for (let turnIndex = 0; turnIndex < session.length; turnIndex += 1) {
759
+ const turn = session[turnIndex] ?? {};
760
+ const turnId = `${sessionId}:${turnIndex}`;
761
+ if (turn.has_answer === true) evidenceTurnIds.add(turnId);
762
+ records.push(recordFromContent({
763
+ id: `longmemeval:${itemId}:${turnId}`,
764
+ dataset_item_id: itemId,
765
+ session_id: sessionId,
766
+ turn_id: turnId,
767
+ role: turn.role ?? undefined,
768
+ kind: 'longmemeval_haystack_turn',
769
+ tags: ['longmemeval', String(item.question_type ?? ''), `session_${sessionId}`],
770
+ has_answer: turn.has_answer === true,
771
+ ordinal,
772
+ content: turn.content ?? turn.text ?? turn.message ?? '',
773
+ }));
774
+ ordinal += 1;
775
+ }
776
+ }
777
+ const answerSessions = answerSessionIds(item);
778
+ queries.push({
779
+ id: `longmemeval:${itemId}`,
780
+ dataset_item_id: itemId,
781
+ question: String(item.question ?? ''),
782
+ question_type: item.question_type === undefined ? undefined : String(item.question_type),
783
+ evidence_turn_ids: evidenceTurnIds,
784
+ evidence_session_ids: answerSessions,
785
+ abstention: isLongMemEvalAbstention(item, answerSessions),
786
+ });
787
+ }
788
+ return {
789
+ id: 'longmemeval',
790
+ label: 'LongMemEval',
791
+ source_url: LONGMEMEVAL_SOURCE_URLS,
792
+ license: 'See Hugging Face dataset card and upstream LongMemEval repository for the selected cleaned file.',
793
+ parser: 'haystack_sessions turns as memory records; has_answer:true turns and answer_session_ids are used as evidence labels; _abs ids are evaluated as abstention cases',
794
+ task_categories: ['information extraction', 'multi-session reasoning', 'temporal reasoning', 'knowledge updates', 'abstention'],
795
+ records,
796
+ queries,
797
+ };
798
+ }
799
+
800
+ function scoreLocomoMethod(method, dataset, topK) {
801
+ const latencies = [];
802
+ let evidenceQuestions = 0;
803
+ let hits = 0;
804
+ let exactCoverage = 0;
805
+ let totalTokens = 0;
806
+ let selectedTotal = 0;
807
+ for (const query of dataset.queries) {
808
+ const start = performance.now();
809
+ const records = dataset.records.filter((record) => record.dataset_item_id === query.dataset_item_id);
810
+ const selected = selectRecords(method.id, records, query, topK);
811
+ latencies.push(performance.now() - start);
812
+ const selectedDialogs = new Set(selected.map((record) => record.dialog_id));
813
+ selectedTotal += selected.length;
814
+ totalTokens += estimatePromptTokens(query.question, selected);
815
+ if (query.evidence_dialog_ids.size > 0) {
816
+ evidenceQuestions += 1;
817
+ let covered = 0;
818
+ for (const evidenceId of query.evidence_dialog_ids) if (selectedDialogs.has(evidenceId)) covered += 1;
819
+ if (covered > 0) hits += 1;
820
+ if (covered === query.evidence_dialog_ids.size) exactCoverage += 1;
821
+ }
822
+ }
823
+ return {
824
+ id: method.id,
825
+ method: method.id,
826
+ local_method_only: true,
827
+ external_provider_called: false,
828
+ retrieval_proxy_only: true,
829
+ uses_top_k: method.uses_top_k,
830
+ top_k: method.uses_top_k ? topK : null,
831
+ question_count: dataset.queries.length,
832
+ evidence_question_count: evidenceQuestions,
833
+ evidence_hit_at_k: rate(hits, evidenceQuestions),
834
+ exact_evidence_coverage: rate(exactCoverage, evidenceQuestions),
835
+ estimated_prompt_tokens: {
836
+ total: totalTokens,
837
+ mean_per_question: mean(totalTokens, dataset.queries.length),
838
+ estimator: 'estimateTextTokens deterministic local estimator',
839
+ },
840
+ selected_memory_count: {
841
+ total: selectedTotal,
842
+ mean_per_question: mean(selectedTotal, dataset.queries.length),
843
+ },
844
+ latency: latencySummary(latencies),
845
+ public_question_text_included: false,
846
+ public_answer_text_included: false,
847
+ raw_conversation_text_included: false,
848
+ };
849
+ }
850
+
851
+ function scoreLongMemEvalMethod(method, dataset, topK) {
852
+ const latencies = [];
853
+ let turnEvidenceQuestions = 0;
854
+ let turnHits = 0;
855
+ let exactTurnCoverage = 0;
856
+ let sessionEvidenceQuestions = 0;
857
+ let sessionHits = 0;
858
+ let exactSessionCoverage = 0;
859
+ let abstentionQuestions = 0;
860
+ let abstentionCorrect = 0;
861
+ let totalTokens = 0;
862
+ let selectedTotal = 0;
863
+ for (const query of dataset.queries) {
864
+ const start = performance.now();
865
+ const records = dataset.records.filter((record) => record.dataset_item_id === query.dataset_item_id);
866
+ const selected = selectRecords(method.id, records, query, topK);
867
+ latencies.push(performance.now() - start);
868
+ const selectedTurns = new Set(selected.map((record) => record.turn_id));
869
+ const selectedSessions = new Set(selected.map((record) => record.session_id));
870
+ selectedTotal += selected.length;
871
+ totalTokens += estimatePromptTokens(query.question, selected);
872
+
873
+ if (query.abstention) {
874
+ abstentionQuestions += 1;
875
+ let selectedGold = false;
876
+ for (const turnId of query.evidence_turn_ids) if (selectedTurns.has(turnId)) selectedGold = true;
877
+ for (const sessionId of query.evidence_session_ids) if (selectedSessions.has(sessionId)) selectedGold = true;
878
+ if (!selectedGold) abstentionCorrect += 1;
879
+ continue;
880
+ }
881
+
882
+ if (query.evidence_turn_ids.size > 0) {
883
+ turnEvidenceQuestions += 1;
884
+ let covered = 0;
885
+ for (const turnId of query.evidence_turn_ids) if (selectedTurns.has(turnId)) covered += 1;
886
+ if (covered > 0) turnHits += 1;
887
+ if (covered === query.evidence_turn_ids.size) exactTurnCoverage += 1;
888
+ }
889
+ if (query.evidence_session_ids.size > 0) {
890
+ sessionEvidenceQuestions += 1;
891
+ let covered = 0;
892
+ for (const sessionId of query.evidence_session_ids) if (selectedSessions.has(sessionId)) covered += 1;
893
+ if (covered > 0) sessionHits += 1;
894
+ if (covered === query.evidence_session_ids.size) exactSessionCoverage += 1;
895
+ }
896
+ }
897
+ return {
898
+ id: method.id,
899
+ method: method.id,
900
+ local_method_only: true,
901
+ external_provider_called: false,
902
+ retrieval_proxy_only: true,
903
+ uses_top_k: method.uses_top_k,
904
+ top_k: method.uses_top_k ? topK : null,
905
+ item_count: dataset.queries.length,
906
+ turn_evidence_question_count: turnEvidenceQuestions,
907
+ turn_evidence_hit_at_k: rate(turnHits, turnEvidenceQuestions),
908
+ exact_turn_evidence_coverage: rate(exactTurnCoverage, turnEvidenceQuestions),
909
+ session_evidence_question_count: sessionEvidenceQuestions,
910
+ session_evidence_hit_at_k: rate(sessionHits, sessionEvidenceQuestions),
911
+ exact_session_evidence_coverage: rate(exactSessionCoverage, sessionEvidenceQuestions),
912
+ abstention_questions: abstentionQuestions,
913
+ abstention_correct: abstentionCorrect,
914
+ abstention_correctness: rate(abstentionCorrect, abstentionQuestions),
915
+ estimated_prompt_tokens: {
916
+ total: totalTokens,
917
+ mean_per_item: mean(totalTokens, dataset.queries.length),
918
+ estimator: 'estimateTextTokens deterministic local estimator',
919
+ },
920
+ selected_memory_count: {
921
+ total: selectedTotal,
922
+ mean_per_item: mean(selectedTotal, dataset.queries.length),
923
+ },
924
+ latency: latencySummary(latencies),
925
+ public_question_text_included: false,
926
+ public_answer_text_included: false,
927
+ raw_conversation_text_included: false,
928
+ };
929
+ }
930
+
931
+ function scoreDataset(dataset, topK) {
932
+ const methodRows = STANDARD_MEMORY_BENCHMARK_METHODS.map((method) => (
933
+ dataset.id === 'locomo' ? scoreLocomoMethod(method, dataset, topK) : scoreLongMemEvalMethod(method, dataset, topK)
934
+ ));
935
+ return {
936
+ id: dataset.id,
937
+ dataset: dataset.id,
938
+ label: dataset.label,
939
+ source_url: dataset.source_url,
940
+ license: dataset.license,
941
+ parser: dataset.parser,
942
+ task_categories: dataset.task_categories,
943
+ record_count: dataset.records.length,
944
+ question_count: dataset.queries.length,
945
+ item_count: dataset.queries.length,
946
+ raw_question_text_included: false,
947
+ raw_answer_text_included: false,
948
+ raw_conversation_text_included: false,
949
+ methods: methodRows,
950
+ };
951
+ }
952
+
953
+ export function runStandardMemoryBenchmarkSuite(options = {}) {
954
+ const topK = optionalPositiveInteger(options.top_k ?? options.topK, 'top_k') ?? 5;
955
+ const datasetRows = [];
956
+ if (options.locomoData !== undefined) {
957
+ datasetRows.push(scoreDataset(parseLocomoDataset(options.locomoData, { max_qa: options.max_locomo_qa ?? options.maxLocomoQa }), topK));
958
+ }
959
+ if (options.longMemEvalData !== undefined || options.longmemevalData !== undefined) {
960
+ datasetRows.push(scoreDataset(parseLongMemEvalDataset(options.longMemEvalData ?? options.longmemevalData, { max_items: options.max_longmemeval_items ?? options.maxLongMemEvalItems }), topK));
961
+ }
962
+ if (datasetRows.length === 0) throw new Error('At least one standard dataset is required: provide locomoData and/or longMemEvalData');
963
+ return buildSuiteReport(datasetRows, topK, options);
964
+ }
965
+
966
+ export async function runStandardMemoryBenchmarkSuiteFromFiles(options = {}) {
967
+ const datasetRows = [];
968
+ const topK = optionalPositiveInteger(options.top_k ?? options.topK, 'top_k') ?? 5;
969
+ if (options.locomo !== undefined || options.locomoPath !== undefined) {
970
+ const path = options.locomo ?? options.locomoPath;
971
+ const loaded = await readJsonWithSha256(path);
972
+ datasetRows.push({
973
+ ...scoreDataset(parseLocomoDataset(loaded.data, { max_qa: options.max_locomo_qa ?? options.maxLocomoQa }), topK),
974
+ local_file_name: publicFileName(path),
975
+ input_sha256: loaded.sha256,
976
+ });
977
+ }
978
+ if (options.longmemeval !== undefined || options.longmemevalPath !== undefined || options.longMemEvalPath !== undefined) {
979
+ const path = options.longmemeval ?? options.longmemevalPath ?? options.longMemEvalPath;
980
+ const maxItems = optionalPositiveInteger(options.max_longmemeval_items ?? options.maxLongMemEvalItems, 'max_longmemeval_items');
981
+ const loaded = await readLongMemEvalJsonWithSha256(path, maxItems);
982
+ datasetRows.push({
983
+ ...scoreDataset(parseLongMemEvalDataset(loaded.data, { max_items: maxItems }), topK),
984
+ local_file_name: publicFileName(path),
985
+ input_sha256: loaded.sha256,
986
+ });
987
+ }
988
+ if (datasetRows.length === 0) throw new Error('Provide --locomo <path> and/or --longmemeval <path>');
989
+ return buildSuiteReport(datasetRows, topK, options);
990
+ }
991
+
992
+ function buildSuiteReport(datasetRows, topK, options) {
993
+ return {
994
+ schema: STANDARD_MEMORY_BENCHMARK_SUITE_SCHEMA,
995
+ generated_at: options.generated_at ?? new Date().toISOString(),
996
+ package: {
997
+ name: 'enigma-memory',
998
+ version: '0.1.15',
999
+ },
1000
+ public_safe: true,
1001
+ top_k: topK,
1002
+ source_urls: {
1003
+ locomo: LOCOMO_SOURCE_URL,
1004
+ longmemeval: LONGMEMEVAL_SOURCE_URLS,
1005
+ },
1006
+ license_and_boundary_notes: [
1007
+ 'LoCoMo source data is CC BY-NC 4.0; keep local dataset files and raw conversations out of public reports unless separately reviewed.',
1008
+ 'LongMemEval cleaned files are operator-supplied local JSON files from the upstream Hugging Face dataset repository.',
1009
+ 'Scores are retrieval/evidence proxy metrics over official dataset labels, not LLM-generated answer accuracy.',
1010
+ 'No provider APIs, hosted runtimes, competitor SDKs, or external accounts are called by this runner.',
1011
+ 'Rows are local deterministic methods only; no third-party competitor scores or benchmark-leadership claims are emitted.',
1012
+ ],
1013
+ benchmark_boundaries: {
1014
+ official_dataset_files_required: true,
1015
+ credentials_required: false,
1016
+ external_provider_calls: false,
1017
+ llm_answer_accuracy_scored: false,
1018
+ retrieval_evidence_proxy_scored: true,
1019
+ raw_question_text_included: false,
1020
+ raw_answer_text_included: false,
1021
+ raw_conversation_text_included: false,
1022
+ provider_deletion_claim: false,
1023
+ model_forgetting_claim: false,
1024
+ roi_or_provider_invoice_savings_claim: false,
1025
+ compliance_certification_claim: false,
1026
+ benchmark_leadership_claim: false,
1027
+ },
1028
+ relevance_logic: {
1029
+ token_extraction: 'keyword_filter uses basic lowercase /[a-z0-9]+(?:[-_][a-z0-9]+)*/ overlap after stopword removal; enigma_relevance additionally applies deterministic suffix stemming for ing, ed, and plural s forms',
1030
+ production_alignment: 'public-safe local approximation of production query-aware retrieval using query/content stems, role/session/kind/tag hints, category/task hints, temporal/date hints, and phrase/proximity boosts; no private memory is emitted',
1031
+ keyword_filter_fallback: 'empty result when no query/content token overlap exists',
1032
+ enigma_relevance_fallback: 'falls back to all local candidates only when no enhanced relevance signal exists, then applies deterministic local ranking and --top-k',
1033
+ provider_api_used: false,
1034
+ llm_used: false,
1035
+ },
1036
+ local_methods: STANDARD_MEMORY_BENCHMARK_METHODS.map((method) => ({ ...method })),
1037
+ datasets: datasetRows,
1038
+ dataset_rows: datasetRows,
1039
+ };
1040
+ }
1041
+
1042
+ function usage() {
1043
+ return `Usage: node scripts/run-standard-memory-benchmarks.mjs [--locomo <path>] [--longmemeval <path>] [--max-locomo-qa <n>] [--max-longmemeval-items <n>] [--top-k <n>] [--out <path>]\n\nProduces schema ${STANDARD_MEMORY_BENCHMARK_SUITE_SCHEMA}. Raw question, answer, and conversation text are never written to the report. With --longmemeval and --max-longmemeval-items, the local top-level JSON array is streamed for hashing and only the requested sample items are parsed.`;
1044
+ }
1045
+
1046
+ async function main() {
1047
+ const options = parseArgs();
1048
+ if (options.help) {
1049
+ console.log(usage());
1050
+ return;
1051
+ }
1052
+ const report = await runStandardMemoryBenchmarkSuiteFromFiles(options);
1053
+ const serialized = `${JSON.stringify(report, null, 2)}\n`;
1054
+ if (options.out) {
1055
+ const outPath = resolve(options.out);
1056
+ await mkdir(dirname(outPath), { recursive: true });
1057
+ await writeFile(outPath, serialized);
1058
+ } else {
1059
+ process.stdout.write(serialized);
1060
+ }
1061
+ }
1062
+
1063
+ const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
1064
+ const modulePath = fileURLToPath(import.meta.url);
1065
+ if (invokedPath === modulePath) {
1066
+ main().catch((error) => {
1067
+ console.error(error instanceof Error ? error.message : String(error));
1068
+ process.exitCode = 1;
1069
+ });
1070
+ }