wendkeep 0.85.1 → 0.86.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.en.md +2 -1
  3. package/README.md +2 -1
  4. package/docs/en/commands/evidence-embeddings.md +243 -0
  5. package/docs/en/commands/mcp.md +67 -7
  6. package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
  7. package/docs/pt-BR/commands/mcp.md +66 -7
  8. package/hooks/evidence-context.mjs +41 -7
  9. package/hooks/evidence-recall.mjs +10 -0
  10. package/package.json +1 -1
  11. package/packages/mcp/src/effects.mjs +3 -2
  12. package/packages/mcp/src/evidence-recall.mjs +130 -0
  13. package/packages/mcp/src/executor.mjs +4 -0
  14. package/packages/mcp/src/server.mjs +31 -1
  15. package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
  16. package/packages/vault/src/evidence-index-store.mjs +360 -0
  17. package/packages/vault/src/evidence-recall-page.mjs +381 -0
  18. package/packages/vault/src/evidence-search-index.mjs +917 -0
  19. package/packages/vault/src/index.mjs +12 -1
  20. package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
  21. package/packages/vault/src/memory-ledger-view.mjs +41 -0
  22. package/packages/vault/src/memory-rotation-store.mjs +967 -0
  23. package/packages/vault/src/memory-segment-store.mjs +820 -0
  24. package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
  25. package/packages/vault/src/memory-store-base.mjs +1161 -0
  26. package/packages/vault/src/memory-store-core.mjs +2 -0
  27. package/packages/vault/src/memory-store.mjs +46 -1161
  28. package/src/doctor.mjs +41 -5
  29. package/src/evidence-search-health.mjs +221 -0
  30. package/src/memory-scale-health.mjs +210 -0
  31. package/src/observer-snapshot.mjs +87 -1
@@ -0,0 +1,967 @@
1
+ import {
2
+ closeSync, fstatSync, fsyncSync, openSync, readdirSync, statSync, writeFileSync,
3
+ } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ import {
7
+ MEMORY_LOCK_BUSY,
8
+ canonicalMemoryJson,
9
+ memoryFileIdentityMatches,
10
+ readMemoryLedger as readActiveMemoryLedger,
11
+ withMemoryLock,
12
+ } from './memory-store-base.mjs';
13
+ import {
14
+ MEMORY_LEDGER_GENERATION_SCHEMA_VERSION,
15
+ MEMORY_ROTATION_JOURNAL_SCHEMA_VERSION,
16
+ MEMORY_ROTATION_RECEIPT_CHECKPOINT_SCHEMA_VERSION,
17
+ MEMORY_ROTATION_RECEIPT_SCHEMA_VERSION,
18
+ MemoryLedgerGenerationCorruption,
19
+ MemoryRotationReceiptCorruption,
20
+ canonicalMemoryControlJson,
21
+ hashMemoryControl,
22
+ memoryLedgerBackupDirectory,
23
+ memoryLedgerGenerationPath,
24
+ memoryLedgerGenerationStatus,
25
+ memoryLedgerPath,
26
+ memoryRotationJournalPath,
27
+ memoryRotationReceiptCheckpointPath,
28
+ memoryRotationReceiptsPath,
29
+ memorySha256,
30
+ parseMemoryLedgerContent,
31
+ projectIdForMemoryLedger,
32
+ readMemoryControlFile,
33
+ readMemoryLedger,
34
+ readMemoryLedgerGeneration,
35
+ readMemoryRotationJournal,
36
+ readMemoryRotationReceipts,
37
+ } from './memory-ledger-view.mjs';
38
+ import {
39
+ readMemorySegmentManifest,
40
+ verifyMemorySegments,
41
+ } from './memory-segment-store.mjs';
42
+ import { readMemoryProjectionSnapshot } from './memory-snapshot-store.mjs';
43
+ import {
44
+ assertVaultPathSafe,
45
+ mkdirVaultPath,
46
+ unlinkVaultFile,
47
+ writeVaultFileAtomic,
48
+ } from './vault-path-safety.mjs';
49
+
50
+ export const MEMORY_ROTATION_CANDIDATE_PREFIX = 'MEMORY_ROTATION_CANDIDATE';
51
+ export const MEMORY_ROTATION_POLICY = 'retain-source-backup';
52
+
53
+ const SHA256 = /^[a-f0-9]{64}$/;
54
+ const OPERATION_ID = /^memrot-[a-f0-9]{16}$/;
55
+ const BACKUP_FILE = /^memory-ledger-generations\/ledger-gen-(\d{6})-([a-f0-9]{16})\.jsonl$/;
56
+ const CANDIDATE_FILE = /^MEMORY_ROTATION_CANDIDATE\.(memrot-[a-f0-9]{16})\.jsonl$/;
57
+ const CHAIN_GENESIS = '0'.repeat(64);
58
+ const RECEIPT_KIND = 'memory-ledger-rotation';
59
+
60
+ export class MemoryLedgerRotationBlocked extends Error {
61
+ constructor(errors, message = 'Memory ledger rotation is blocked; inspect the dry-run and recover any pending journal.') {
62
+ super(message);
63
+ this.name = 'MemoryLedgerRotationBlocked';
64
+ this.code = 'MEMORY_LEDGER_ROTATION_BLOCKED';
65
+ this.errors = Array.isArray(errors) ? errors : [String(errors || 'unknown rotation blocker')];
66
+ }
67
+ }
68
+
69
+ function brainDir(vaultBase) { return join(vaultBase, '.brain'); }
70
+ function outboxDir(vaultBase) { return join(brainDir(vaultBase), 'memory-outbox'); }
71
+ function snapshotPath(vaultBase) { return join(brainDir(vaultBase), 'MEMORY_SNAPSHOT.json'); }
72
+ function candidateRelative(operationId) { return `${MEMORY_ROTATION_CANDIDATE_PREFIX}.${operationId}.jsonl`; }
73
+ function candidatePath(vaultBase, operationId) { return join(brainDir(vaultBase), candidateRelative(operationId)); }
74
+ function backupRelative(generation, hash) {
75
+ return `memory-ledger-generations/ledger-gen-${String(generation).padStart(6, '0')}-${hash.slice(0, 16)}.jsonl`;
76
+ }
77
+ function backupPath(vaultBase, relativePath) {
78
+ if (!BACKUP_FILE.test(String(relativePath || '')) || String(relativePath).includes('..') || String(relativePath).includes('\\')) {
79
+ throw new MemoryLedgerRotationBlocked([`unsafe backup path: ${relativePath}`]);
80
+ }
81
+ return join(brainDir(vaultBase), ...String(relativePath).split('/'));
82
+ }
83
+
84
+ function checkedFile(vaultBase, path, label, { allowMissing = true, mustNotExist = false } = {}) {
85
+ return assertVaultPathSafe(vaultBase, path, {
86
+ allowMissing,
87
+ expectedType: 'file',
88
+ mustNotExist,
89
+ label,
90
+ });
91
+ }
92
+
93
+ function checkedDirectory(vaultBase, path, label, { allowMissing = true } = {}) {
94
+ return assertVaultPathSafe(vaultBase, path, {
95
+ allowMissing,
96
+ expectedType: 'directory',
97
+ label,
98
+ });
99
+ }
100
+
101
+ function assertOpenedFile(vaultBase, path, fd, label) {
102
+ const checked = checkedFile(vaultBase, path, label, { allowMissing: false });
103
+ const descriptor = fstatSync(fd, { bigint: true });
104
+ const target = statSync(checked.target, { bigint: true });
105
+ if (!descriptor.isFile() || descriptor.nlink > 1n || target.nlink > 1n
106
+ || !memoryFileIdentityMatches(descriptor, target)) {
107
+ const error = new Error(`${label} mudou de inode ou possui hardlink antes da mutação.`);
108
+ error.code = 'VAULT_PATH_UNSAFE';
109
+ throw error;
110
+ }
111
+ return checked.target;
112
+ }
113
+
114
+ function canonicalLedger(events) {
115
+ return events.map((event) => canonicalMemoryJson(event)).join('\n') + (events.length ? '\n' : '');
116
+ }
117
+
118
+ function snapshotHash(snapshot) {
119
+ return hashMemoryControl(snapshot, 'snapshot_hash');
120
+ }
121
+
122
+ function sanitizedText(value, max) {
123
+ return String(value || '').trim().replace(/[\r\n\t]+/g, ' ').replace(/\s{2,}/g, ' ').slice(0, max);
124
+ }
125
+
126
+ function authorization(options = {}, { required = false } = {}) {
127
+ const reason = sanitizedText(options.reason, 500);
128
+ const authorizedBy = sanitizedText(options.authorizedBy ?? options.authorized_by, 200);
129
+ const errors = [];
130
+ if (required && reason.length < 3) errors.push('reason is required for rotation apply');
131
+ if (required && authorizedBy.length < 2) errors.push('authorizedBy is required for rotation apply');
132
+ return { reason, authorizedBy, errors };
133
+ }
134
+
135
+ function now(options = {}) {
136
+ const value = typeof options.now === 'function' ? options.now() : (options.now || new Date().toISOString());
137
+ const text = String(value || '');
138
+ if (Number.isNaN(Date.parse(text))) throw new TypeError('rotation now must be an ISO timestamp');
139
+ return new Date(text).toISOString();
140
+ }
141
+
142
+ function countPendingOutbox(vaultBase) {
143
+ let checked = checkedDirectory(vaultBase, outboxDir(vaultBase), 'outbox da rotação do ledger');
144
+ if (!checked.exists) return { pending: 0, unknown: [] };
145
+ checked = checkedDirectory(vaultBase, checked.target, 'outbox da rotação do ledger', {
146
+ allowMissing: false,
147
+ });
148
+ const unknown = [];
149
+ let pending = 0;
150
+ for (const entry of readdirSync(checked.target, { withFileTypes: true })) {
151
+ const path = join(checked.target, entry.name);
152
+ assertVaultPathSafe(vaultBase, path, {
153
+ allowMissing: false,
154
+ label: `entrada ${entry.name} do outbox da rotação`,
155
+ });
156
+ if (entry.isFile() && entry.name.endsWith('.json')) pending += 1;
157
+ else unknown.push(entry.name);
158
+ }
159
+ return { pending, unknown };
160
+ }
161
+
162
+ function readSnapshotRaw(vaultBase) {
163
+ return readMemoryControlFile(
164
+ vaultBase,
165
+ snapshotPath(vaultBase),
166
+ 'snapshot da rotação do ledger',
167
+ { allowMissing: false },
168
+ );
169
+ }
170
+
171
+ function transformSnapshot(
172
+ snapshot,
173
+ generation,
174
+ operationId,
175
+ sourceLedgerHash,
176
+ anchorLine,
177
+ anchorEventId,
178
+ ) {
179
+ const transformed = structuredClone(snapshot);
180
+ const lineBytes = Buffer.byteLength(anchorLine);
181
+ transformed.ledger_bytes = lineBytes + 1;
182
+ transformed.through_line_start = 0;
183
+ transformed.through_line_length = lineBytes;
184
+ transformed.through_line_hash = memorySha256(anchorLine);
185
+ transformed.through_event_id = anchorEventId;
186
+ transformed.ledger_generation = generation;
187
+ transformed.ledger_generation_operation_id = operationId;
188
+ transformed.ledger_generation_source_hash = sourceLedgerHash;
189
+ delete transformed.ledger_generation_state_hash;
190
+ transformed.snapshot_hash = snapshotHash(transformed);
191
+ return transformed;
192
+ }
193
+
194
+ function receiptFacts(plan) {
195
+ return {
196
+ schema_version: MEMORY_ROTATION_RECEIPT_SCHEMA_VERSION,
197
+ kind: RECEIPT_KIND,
198
+ operation_id: plan.operationId,
199
+ project_id: plan.projectId,
200
+ generation: plan.generation,
201
+ previous_generation: plan.previousGeneration,
202
+ policy: MEMORY_ROTATION_POLICY,
203
+ source_ledger_hash: plan.sourceLedgerHash,
204
+ source_event_count: plan.sourceEventCount,
205
+ segment_manifest_hash: plan.segmentManifestHash,
206
+ segment_chain_tip: plan.segmentChainTip,
207
+ snapshot_hash: plan.nextSnapshot.snapshot_hash,
208
+ source_snapshot_hash: plan.sourceSnapshotHash,
209
+ backup_file: plan.backupFile,
210
+ backup_hash: plan.backupHash,
211
+ active_ledger_hash: plan.candidateHash,
212
+ anchor_event_id: plan.anchorEventId,
213
+ generation_state_hash: plan.generationState.state_hash,
214
+ reason: plan.reason,
215
+ authorized_by: plan.authorizedBy,
216
+ completed_at: plan.completedAt,
217
+ };
218
+ }
219
+
220
+ function operationIdFor(subject) {
221
+ return `memrot-${memorySha256(canonicalMemoryControlJson(subject)).slice(0, 16)}`;
222
+ }
223
+
224
+ function generationCandidate(vaultBase, generationState) {
225
+ if (!generationState?.operation_id) return { exists: false, path: null };
226
+ const path = candidatePath(vaultBase, generationState.operation_id);
227
+ const checked = checkedFile(vaultBase, path, 'candidate residual da rotação', { allowMissing: true });
228
+ return { exists: checked.exists, path: checked.target };
229
+ }
230
+
231
+ function buildPlan(vaultBase, options = {}, { allowJournal = false } = {}) {
232
+ const blockers = [];
233
+ const projectId = projectIdForMemoryLedger(vaultBase);
234
+ const journal = readMemoryRotationJournal(vaultBase);
235
+ if (!allowJournal && journal.status !== 'missing') {
236
+ blockers.push(journal.status === 'ok'
237
+ ? `rotation recovery required for ${journal.journal.operation_id} at stage ${journal.journal.stage}`
238
+ : `rotation journal invalid: ${journal.errors.join('; ')}`);
239
+ }
240
+
241
+ const auth = authorization(options);
242
+ const outbox = countPendingOutbox(vaultBase);
243
+ if (outbox.pending) blockers.push(`${outbox.pending} pending memory outbox event(s)`);
244
+ if (outbox.unknown.length) blockers.push(`unknown outbox entries: ${outbox.unknown.join(', ')}`);
245
+
246
+ const generation = readMemoryLedgerGeneration(vaultBase);
247
+ if (generation.status === 'invalid') blockers.push(`generation state invalid: ${generation.errors.join('; ')}`);
248
+ if (generation.status === 'ok' && generationCandidate(vaultBase, generation.state).exists) {
249
+ blockers.push(`completed rotation candidate cleanup required for ${generation.state.operation_id}`);
250
+ }
251
+ const previousGeneration = generation.status === 'ok' ? generation.state.generation : 0;
252
+ const previousStateHash = generation.status === 'ok' ? generation.state.state_hash : CHAIN_GENESIS;
253
+
254
+ const source = readMemoryLedger(vaultBase);
255
+ if (source.status !== 'ok') blockers.push(...source.errors.map((error) => `ledger: ${error.message}`));
256
+ if (!source.events.length) blockers.push('ledger has no events to rotate');
257
+ if (generation.status === 'ok'
258
+ && source.events.length === generation.state.source_event_count
259
+ && Number(source.activeTailEvents?.length || 0) === 0) {
260
+ blockers.push('current generation has no new events to rotate');
261
+ }
262
+
263
+ const snapshot = readMemoryProjectionSnapshot(vaultBase);
264
+ if (snapshot.status !== 'ok' || snapshot.tail?.status !== 'ok') {
265
+ blockers.push(`snapshot invalid: ${snapshot.reason || snapshot.tail?.reason || snapshot.status}`);
266
+ } else if (source.status === 'ok') {
267
+ if (snapshot.snapshot.event_count !== source.events.length) blockers.push('snapshot event_count does not cover current ledger');
268
+ if (snapshot.tail.events.length !== 0) blockers.push('snapshot has an unapplied tail; project and advance it before rotation');
269
+ }
270
+
271
+ const segments = verifyMemorySegments(vaultBase);
272
+ if (segments.status !== 'ok') blockers.push(`segments invalid: ${(segments.errors || []).join('; ') || segments.status}`);
273
+ const manifest = readMemorySegmentManifest(vaultBase);
274
+ if (manifest.status !== 'ok') blockers.push(`segment manifest unavailable: ${(manifest.errors || []).join('; ') || manifest.status}`);
275
+ if (source.status === 'ok' && segments.status === 'ok' && segments.coveredEvents !== source.events.length) {
276
+ blockers.push(`segment chain covers ${segments.coveredEvents} of ${source.events.length} events; seal with force first`);
277
+ }
278
+
279
+ const active = readActiveMemoryLedger(vaultBase);
280
+ if (active.status !== 'ok') blockers.push(...active.errors.map((error) => `active ledger: ${error.message}`));
281
+
282
+ const completedAt = now(options);
283
+ const generationNumber = previousGeneration + 1;
284
+ const sourceContent = source.status === 'ok' ? canonicalLedger(source.events) : '';
285
+ const sourceLedgerHash = memorySha256(sourceContent);
286
+ const anchor = source.events.at(-1);
287
+ const anchorLine = anchor ? canonicalMemoryJson(anchor) : '';
288
+ const candidateContent = anchor ? `${anchorLine}\n` : '';
289
+ const candidateHash = memorySha256(candidateContent);
290
+ const backupFile = backupRelative(generationNumber, sourceLedgerHash);
291
+ const sourceSnapshotHash = snapshot.status === 'ok' ? snapshot.snapshot.snapshot_hash : CHAIN_GENESIS;
292
+ const segmentManifestHash = manifest.status === 'ok' ? manifest.manifest.manifest_hash : CHAIN_GENESIS;
293
+ const segmentChainTip = manifest.status === 'ok' ? manifest.manifest.chain_tip : CHAIN_GENESIS;
294
+ const operationId = operationIdFor({
295
+ project_id: projectId,
296
+ generation: generationNumber,
297
+ previous_state_hash: previousStateHash,
298
+ source_ledger_hash: sourceLedgerHash,
299
+ source_event_count: source.events.length,
300
+ segment_manifest_hash: segmentManifestHash,
301
+ snapshot_hash: sourceSnapshotHash,
302
+ });
303
+ const nextSnapshot = snapshot.status === 'ok' && anchor
304
+ ? transformSnapshot(
305
+ snapshot.snapshot,
306
+ generationNumber,
307
+ operationId,
308
+ sourceLedgerHash,
309
+ anchorLine,
310
+ anchor.event_id,
311
+ )
312
+ : null;
313
+ const generationState = {
314
+ schema_version: MEMORY_LEDGER_GENERATION_SCHEMA_VERSION,
315
+ project_id: projectId,
316
+ generation: generationNumber,
317
+ previous_generation: previousGeneration,
318
+ operation_id: operationId,
319
+ policy: MEMORY_ROTATION_POLICY,
320
+ backup_file: backupFile,
321
+ backup_hash: sourceLedgerHash,
322
+ source_ledger_hash: sourceLedgerHash,
323
+ source_event_count: source.events.length,
324
+ anchor_event_id: anchor?.event_id || '',
325
+ anchor_payload_hash: anchor ? memorySha256(anchorLine) : CHAIN_GENESIS,
326
+ active_ledger_hash: candidateHash,
327
+ segment_manifest_hash: segmentManifestHash,
328
+ segment_chain_tip: segmentChainTip,
329
+ source_snapshot_hash: sourceSnapshotHash,
330
+ snapshot_hash: nextSnapshot?.snapshot_hash || CHAIN_GENESIS,
331
+ previous_state_hash: previousStateHash,
332
+ rotated_at: completedAt,
333
+ };
334
+ generationState.state_hash = hashMemoryControl(generationState, 'state_hash');
335
+
336
+ const sourceActiveHash = active.status === 'ok' ? memorySha256(active.raw) : CHAIN_GENESIS;
337
+ const plan = {
338
+ projectId,
339
+ operationId,
340
+ generation: generationNumber,
341
+ previousGeneration,
342
+ previousStateHash,
343
+ sourceContent,
344
+ sourceLedgerHash,
345
+ sourceEventCount: source.events.length,
346
+ sourceActiveHash,
347
+ sourceActiveBytes: active.status === 'ok' ? Buffer.byteLength(active.raw) : 0,
348
+ sourceSnapshotHash,
349
+ segmentManifestHash,
350
+ segmentChainTip,
351
+ backupFile,
352
+ backupHash: sourceLedgerHash,
353
+ candidateFile: candidateRelative(operationId),
354
+ candidateContent,
355
+ candidateHash,
356
+ anchorEventId: anchor?.event_id || '',
357
+ generationState,
358
+ nextSnapshot,
359
+ reason: auth.reason,
360
+ authorizedBy: auth.authorizedBy,
361
+ completedAt,
362
+ };
363
+
364
+ return {
365
+ status: blockers.length ? 'blocked' : 'preview',
366
+ apply: false,
367
+ blockers,
368
+ plan,
369
+ sourceEvents: source.events.length,
370
+ sourceBytes: Buffer.byteLength(sourceContent),
371
+ activeBytesBefore: plan.sourceActiveBytes,
372
+ activeBytesAfter: Buffer.byteLength(candidateContent),
373
+ reclaimedActiveBytes: Math.max(0, plan.sourceActiveBytes - Buffer.byteLength(candidateContent)),
374
+ backupBytes: Buffer.byteLength(sourceContent),
375
+ generation: generationNumber,
376
+ operationId,
377
+ policy: MEMORY_ROTATION_POLICY,
378
+ };
379
+ }
380
+
381
+ export function planMemoryLedgerRotation(vaultBase, options = {}) {
382
+ return buildPlan(vaultBase, options);
383
+ }
384
+
385
+ function renderControl(value) {
386
+ return `${JSON.stringify(value, null, 2)}\n`;
387
+ }
388
+
389
+ function writeImmutableFile(vaultBase, path, content, label, scopeRoot) {
390
+ const checked = checkedFile(vaultBase, path, label);
391
+ if (checked.exists) {
392
+ const current = readMemoryControlFile(vaultBase, checked.target, label, { allowMissing: false });
393
+ if (current !== content) throw new MemoryLedgerRotationBlocked([`${label} exists with divergent bytes`]);
394
+ return 'existing';
395
+ }
396
+ writeVaultFileAtomic(vaultBase, path, content, 'utf8', {
397
+ label,
398
+ scopeRoot,
399
+ beforeRename: () => checkedFile(vaultBase, path, label, { mustNotExist: true }),
400
+ });
401
+ return 'written';
402
+ }
403
+
404
+ function writeJournal(vaultBase, journal) {
405
+ const value = { ...journal, journal_hash: '' };
406
+ value.journal_hash = hashMemoryControl(value, 'journal_hash');
407
+ writeVaultFileAtomic(
408
+ vaultBase,
409
+ memoryRotationJournalPath(vaultBase),
410
+ renderControl(value),
411
+ 'utf8',
412
+ { label: 'journal da rotação do ledger', scopeRoot: brainDir(vaultBase) },
413
+ );
414
+ return value;
415
+ }
416
+
417
+ function journalForPlan(plan) {
418
+ return writeJournalShape(plan, 'prepared', plan.completedAt);
419
+ }
420
+
421
+ function writeJournalShape(plan, stage, updatedAt) {
422
+ return {
423
+ schema_version: MEMORY_ROTATION_JOURNAL_SCHEMA_VERSION,
424
+ project_id: plan.projectId,
425
+ operation_id: plan.operationId,
426
+ stage,
427
+ created_at: plan.completedAt,
428
+ updated_at: updatedAt,
429
+ plan: {
430
+ generation: plan.generation,
431
+ previous_generation: plan.previousGeneration,
432
+ previous_state_hash: plan.previousStateHash,
433
+ source_ledger_hash: plan.sourceLedgerHash,
434
+ source_event_count: plan.sourceEventCount,
435
+ source_active_hash: plan.sourceActiveHash,
436
+ source_active_bytes: plan.sourceActiveBytes,
437
+ source_snapshot_hash: plan.sourceSnapshotHash,
438
+ segment_manifest_hash: plan.segmentManifestHash,
439
+ segment_chain_tip: plan.segmentChainTip,
440
+ backup_file: plan.backupFile,
441
+ backup_hash: plan.backupHash,
442
+ candidate_file: plan.candidateFile,
443
+ candidate_hash: plan.candidateHash,
444
+ anchor_event_id: plan.anchorEventId,
445
+ generation_state: plan.generationState,
446
+ next_snapshot: plan.nextSnapshot,
447
+ reason: plan.reason,
448
+ authorized_by: plan.authorizedBy,
449
+ completed_at: plan.completedAt,
450
+ },
451
+ };
452
+ }
453
+
454
+ function updateJournalStage(vaultBase, journal, stage) {
455
+ return writeJournal(vaultBase, {
456
+ ...journal,
457
+ stage,
458
+ updated_at: new Date().toISOString(),
459
+ });
460
+ }
461
+
462
+ function injectFault(faultAt, boundary) {
463
+ if (faultAt === boundary) throw new Error(`Injected memory-rotation fault: ${boundary}`);
464
+ }
465
+
466
+ function generationStateMatches(current, expected) {
467
+ return current.status === 'ok'
468
+ && canonicalMemoryControlJson(current.state) === canonicalMemoryControlJson(expected);
469
+ }
470
+
471
+ function currentStateHash(vaultBase) {
472
+ const current = readMemoryLedgerGeneration(vaultBase);
473
+ if (current.status === 'missing') return CHAIN_GENESIS;
474
+ if (current.status !== 'ok') throw new MemoryLedgerGenerationCorruption(current.errors);
475
+ return current.state.state_hash;
476
+ }
477
+
478
+ function switchActiveLedger(vaultBase, plan) {
479
+ const active = readActiveMemoryLedger(vaultBase);
480
+ if (active.status !== 'ok') throw new MemoryLedgerRotationBlocked(active.errors.map((error) => error.message));
481
+ const currentHash = memorySha256(active.raw);
482
+ if (currentHash === plan.candidateHash) return 'existing';
483
+ if (currentHash !== plan.sourceActiveHash) {
484
+ throw new MemoryLedgerRotationBlocked(['active ledger changed after rotation plan']);
485
+ }
486
+ writeVaultFileAtomic(
487
+ vaultBase,
488
+ memoryLedgerPath(vaultBase),
489
+ plan.candidateContent,
490
+ 'utf8',
491
+ {
492
+ label: 'active ledger generation switch',
493
+ scopeRoot: brainDir(vaultBase),
494
+ beforeRename: () => {
495
+ const fresh = readActiveMemoryLedger(vaultBase);
496
+ if (fresh.status !== 'ok' || memorySha256(fresh.raw) !== plan.sourceActiveHash) {
497
+ throw new MemoryLedgerRotationBlocked(['active ledger changed at switch boundary']);
498
+ }
499
+ if (currentStateHash(vaultBase) !== plan.previousStateHash) {
500
+ throw new MemoryLedgerRotationBlocked(['generation state changed at switch boundary']);
501
+ }
502
+ },
503
+ },
504
+ );
505
+ return 'written';
506
+ }
507
+
508
+ function ensureGenerationState(vaultBase, expected) {
509
+ const current = readMemoryLedgerGeneration(vaultBase);
510
+ if (current.status === 'ok') {
511
+ if (!generationStateMatches(current, expected)) {
512
+ throw new MemoryLedgerRotationBlocked(['generation state diverges from rotation journal']);
513
+ }
514
+ return 'existing';
515
+ }
516
+ if (current.status !== 'missing') throw new MemoryLedgerGenerationCorruption(current.errors);
517
+ writeVaultFileAtomic(
518
+ vaultBase,
519
+ memoryLedgerGenerationPath(vaultBase),
520
+ renderControl(expected),
521
+ 'utf8',
522
+ { label: 'estado da geração do ledger', scopeRoot: brainDir(vaultBase) },
523
+ );
524
+ return 'written';
525
+ }
526
+
527
+ function ensureSnapshot(vaultBase, expected, sourceSnapshotHash) {
528
+ const raw = readSnapshotRaw(vaultBase);
529
+ let current;
530
+ try { current = JSON.parse(raw); } catch { throw new MemoryLedgerRotationBlocked(['snapshot became invalid during rotation']); }
531
+ if (current.snapshot_hash === expected.snapshot_hash
532
+ && canonicalMemoryControlJson(current) === canonicalMemoryControlJson(expected)) return 'existing';
533
+ if (current.snapshot_hash !== sourceSnapshotHash) {
534
+ throw new MemoryLedgerRotationBlocked(['snapshot changed after rotation plan']);
535
+ }
536
+ writeVaultFileAtomic(
537
+ vaultBase,
538
+ snapshotPath(vaultBase),
539
+ renderControl(expected),
540
+ 'utf8',
541
+ { label: 'snapshot reanchored to active generation', scopeRoot: brainDir(vaultBase) },
542
+ );
543
+ return 'written';
544
+ }
545
+
546
+ function receiptRecord(plan, sequence, previousHash) {
547
+ const receipt = {
548
+ ...receiptFacts(plan),
549
+ sequence,
550
+ previous_receipt_hash: previousHash,
551
+ };
552
+ receipt.receipt_hash = hashMemoryControl(receipt, 'receipt_hash');
553
+ return receipt;
554
+ }
555
+
556
+ function receiptEquivalent(receipt, plan) {
557
+ const facts = receiptFacts(plan);
558
+ return Object.entries(facts).every(([key, value]) => (
559
+ canonicalMemoryControlJson(receipt[key]) === canonicalMemoryControlJson(value)
560
+ ));
561
+ }
562
+
563
+ function appendReceiptLine(vaultBase, line) {
564
+ const path = memoryRotationReceiptsPath(vaultBase);
565
+ let fd;
566
+ try {
567
+ let checked = checkedFile(vaultBase, path, 'ledger de receipts da rotação');
568
+ checked = checkedFile(vaultBase, checked.target, 'ledger de receipts da rotação');
569
+ fd = openSync(checked.target, 'a');
570
+ assertOpenedFile(vaultBase, checked.target, fd, 'ledger de receipts da rotação');
571
+ writeFileSync(fd, line, 'utf8');
572
+ fsyncSync(fd);
573
+ assertOpenedFile(vaultBase, checked.target, fd, 'ledger de receipts da rotação');
574
+ } finally {
575
+ if (fd !== undefined) closeSync(fd);
576
+ }
577
+ }
578
+
579
+ function writeReceiptCheckpoint(vaultBase, receipts) {
580
+ const raw = readMemoryControlFile(
581
+ vaultBase,
582
+ memoryRotationReceiptsPath(vaultBase),
583
+ 'ledger de receipts da rotação',
584
+ { allowMissing: true },
585
+ ) ?? '';
586
+ const checkpoint = {
587
+ schema_version: MEMORY_ROTATION_RECEIPT_CHECKPOINT_SCHEMA_VERSION,
588
+ project_id: projectIdForMemoryLedger(vaultBase),
589
+ count: receipts.receipts.length,
590
+ last_hash: receipts.lastHash,
591
+ file_bytes: Buffer.byteLength(raw),
592
+ };
593
+ checkpoint.checkpoint_hash = hashMemoryControl(checkpoint, 'checkpoint_hash');
594
+ writeVaultFileAtomic(
595
+ vaultBase,
596
+ memoryRotationReceiptCheckpointPath(vaultBase),
597
+ renderControl(checkpoint),
598
+ 'utf8',
599
+ { label: 'checkpoint dos receipts da rotação', scopeRoot: brainDir(vaultBase) },
600
+ );
601
+ return checkpoint;
602
+ }
603
+
604
+ function ensureReceipt(vaultBase, plan) {
605
+ let receipts = readMemoryRotationReceipts(vaultBase);
606
+ if (receipts.status !== 'ok') throw new MemoryRotationReceiptCorruption(receipts.errors);
607
+ const existing = receipts.receipts.find((receipt) => receipt.operation_id === plan.operationId);
608
+ if (existing) {
609
+ if (!receiptEquivalent(existing, plan)) {
610
+ throw new MemoryRotationReceiptCorruption([`operation_id reused with divergent receipt: ${plan.operationId}`]);
611
+ }
612
+ if (receipts.checkpointStatus !== 'ok') writeReceiptCheckpoint(vaultBase, receipts);
613
+ return { status: 'existing', receipt: existing };
614
+ }
615
+ const receipt = receiptRecord(plan, receipts.receipts.length + 1, receipts.lastHash);
616
+ appendReceiptLine(vaultBase, `${canonicalMemoryControlJson(receipt)}\n`);
617
+ receipts = readMemoryRotationReceipts(vaultBase);
618
+ if (receipts.status !== 'ok') throw new MemoryRotationReceiptCorruption(receipts.errors);
619
+ const persisted = receipts.receipts.find((item) => item.operation_id === plan.operationId);
620
+ if (!persisted || !receiptEquivalent(persisted, plan)) {
621
+ throw new MemoryRotationReceiptCorruption(['persisted receipt does not match rotation plan']);
622
+ }
623
+ writeReceiptCheckpoint(vaultBase, receipts);
624
+ return { status: 'written', receipt: persisted };
625
+ }
626
+
627
+ function planFromJournal(vaultBase, journal) {
628
+ const value = journal.plan || {};
629
+ const generationState = value.generation_state;
630
+ const nextSnapshot = value.next_snapshot;
631
+ const plan = {
632
+ projectId: journal.project_id,
633
+ operationId: journal.operation_id,
634
+ generation: value.generation,
635
+ previousGeneration: value.previous_generation,
636
+ previousStateHash: value.previous_state_hash,
637
+ sourceLedgerHash: value.source_ledger_hash,
638
+ sourceEventCount: value.source_event_count,
639
+ sourceActiveHash: value.source_active_hash,
640
+ sourceActiveBytes: value.source_active_bytes,
641
+ sourceSnapshotHash: value.source_snapshot_hash,
642
+ segmentManifestHash: value.segment_manifest_hash,
643
+ segmentChainTip: value.segment_chain_tip,
644
+ backupFile: value.backup_file,
645
+ backupHash: value.backup_hash,
646
+ candidateFile: value.candidate_file,
647
+ candidateHash: value.candidate_hash,
648
+ anchorEventId: value.anchor_event_id,
649
+ generationState,
650
+ nextSnapshot,
651
+ reason: value.reason,
652
+ authorizedBy: value.authorized_by,
653
+ completedAt: value.completed_at,
654
+ candidateContent: '',
655
+ sourceContent: '',
656
+ };
657
+ const errors = [];
658
+ if (!OPERATION_ID.test(String(plan.operationId || ''))) errors.push('journal operation id invalid');
659
+ if (!BACKUP_FILE.test(String(plan.backupFile || ''))) errors.push('journal backup file invalid');
660
+ if (!CANDIDATE_FILE.test(String(plan.candidateFile || '')) || !plan.candidateFile.includes(plan.operationId)) {
661
+ errors.push('journal candidate file invalid');
662
+ }
663
+ if (!Number.isInteger(plan.sourceActiveBytes) || plan.sourceActiveBytes < 0) {
664
+ errors.push('journal source_active_bytes invalid');
665
+ }
666
+ if (!generationState || generationState.state_hash !== hashMemoryControl(generationState, 'state_hash')) {
667
+ errors.push('journal generation state invalid');
668
+ }
669
+ if (!nextSnapshot || nextSnapshot.snapshot_hash !== snapshotHash(nextSnapshot)) {
670
+ errors.push('journal snapshot invalid');
671
+ } else {
672
+ if (nextSnapshot.ledger_generation !== plan.generation) errors.push('journal snapshot generation mismatch');
673
+ if (nextSnapshot.ledger_generation_operation_id !== plan.operationId) {
674
+ errors.push('journal snapshot operation mismatch');
675
+ }
676
+ if (nextSnapshot.ledger_generation_source_hash !== plan.sourceLedgerHash) {
677
+ errors.push('journal snapshot source mismatch');
678
+ }
679
+ if (Object.hasOwn(nextSnapshot, 'ledger_generation_state_hash')) {
680
+ errors.push('journal snapshot contains circular generation state binding');
681
+ }
682
+ }
683
+ if (generationState?.snapshot_hash !== nextSnapshot?.snapshot_hash) {
684
+ errors.push('journal state/snapshot hash mismatch');
685
+ }
686
+ for (const [key, item] of Object.entries({
687
+ sourceLedgerHash: plan.sourceLedgerHash,
688
+ sourceActiveHash: plan.sourceActiveHash,
689
+ sourceSnapshotHash: plan.sourceSnapshotHash,
690
+ segmentManifestHash: plan.segmentManifestHash,
691
+ segmentChainTip: plan.segmentChainTip,
692
+ backupHash: plan.backupHash,
693
+ candidateHash: plan.candidateHash,
694
+ })) {
695
+ if (!SHA256.test(String(item || ''))) errors.push(`journal ${key} invalid`);
696
+ }
697
+ if (errors.length) throw new MemoryLedgerRotationBlocked(errors);
698
+
699
+ const backup = readMemoryControlFile(
700
+ vaultBase,
701
+ backupPath(vaultBase, plan.backupFile),
702
+ 'backup da rotação pendente',
703
+ { allowMissing: false },
704
+ );
705
+ if (memorySha256(backup) !== plan.backupHash || memorySha256(backup) !== plan.sourceLedgerHash) {
706
+ throw new MemoryLedgerRotationBlocked(['journal backup hash mismatch']);
707
+ }
708
+ const parsedBackup = parseMemoryLedgerContent(backup, plan.projectId, plan.backupFile);
709
+ if (parsedBackup.status !== 'ok' || parsedBackup.events.length !== plan.sourceEventCount) {
710
+ throw new MemoryLedgerRotationBlocked(['journal backup authority invalid']);
711
+ }
712
+ const candidate = readMemoryControlFile(
713
+ vaultBase,
714
+ join(brainDir(vaultBase), plan.candidateFile),
715
+ 'candidate da rotação pendente',
716
+ { allowMissing: false },
717
+ );
718
+ if (memorySha256(candidate) !== plan.candidateHash) throw new MemoryLedgerRotationBlocked(['journal candidate hash mismatch']);
719
+ const candidateParsed = parseMemoryLedgerContent(candidate, plan.projectId, plan.candidateFile);
720
+ if (candidateParsed.status !== 'ok' || candidateParsed.events.length !== 1
721
+ || candidateParsed.events[0].event_id !== plan.anchorEventId) {
722
+ throw new MemoryLedgerRotationBlocked(['journal candidate anchor invalid']);
723
+ }
724
+ plan.sourceContent = backup;
725
+ plan.candidateContent = candidate;
726
+
727
+ const manifest = readMemorySegmentManifest(vaultBase);
728
+ if (manifest.status !== 'ok'
729
+ || manifest.manifest.manifest_hash !== plan.segmentManifestHash
730
+ || manifest.manifest.chain_tip !== plan.segmentChainTip
731
+ || manifest.manifest.covered_event_count !== plan.sourceEventCount) {
732
+ throw new MemoryLedgerRotationBlocked(['segment manifest changed after rotation journal']);
733
+ }
734
+ const segments = verifyMemorySegments(vaultBase, { verifyLedger: false, verifySnapshot: false });
735
+ if (segments.status !== 'ok') throw new MemoryLedgerRotationBlocked(segments.errors || ['segment verification failed']);
736
+ return plan;
737
+ }
738
+
739
+ function cleanupOperation(vaultBase, plan, options = {}) {
740
+ unlinkVaultFile(vaultBase, memoryRotationJournalPath(vaultBase), {
741
+ missingOk: true,
742
+ label: 'journal finalizado da rotação',
743
+ });
744
+ injectFault(options.faultAt, 'after-journal-removal');
745
+ unlinkVaultFile(vaultBase, join(brainDir(vaultBase), plan.candidateFile), {
746
+ missingOk: true,
747
+ label: 'candidate finalizado da rotação',
748
+ });
749
+ }
750
+
751
+ function resumeLocked(vaultBase, journal, options = {}) {
752
+ const plan = planFromJournal(vaultBase, journal);
753
+ let currentJournal = journal;
754
+ const switchStatus = switchActiveLedger(vaultBase, plan);
755
+ if (currentJournal.stage === 'prepared') {
756
+ currentJournal = updateJournalStage(vaultBase, currentJournal, 'switched');
757
+ }
758
+ injectFault(options.faultAt, 'after-switch');
759
+
760
+ const stateStatus = ensureGenerationState(vaultBase, plan.generationState);
761
+ if (['prepared', 'switched'].includes(currentJournal.stage)) {
762
+ currentJournal = updateJournalStage(vaultBase, currentJournal, 'state-published');
763
+ }
764
+ injectFault(options.faultAt, 'after-state');
765
+
766
+ const snapshotStatus = ensureSnapshot(vaultBase, plan.nextSnapshot, plan.sourceSnapshotHash);
767
+ if (['prepared', 'switched', 'state-published'].includes(currentJournal.stage)) {
768
+ currentJournal = updateJournalStage(vaultBase, currentJournal, 'snapshot-published');
769
+ }
770
+ injectFault(options.faultAt, 'after-snapshot');
771
+
772
+ const receipt = ensureReceipt(vaultBase, plan);
773
+ if (currentJournal.stage !== 'receipt-published') {
774
+ currentJournal = updateJournalStage(vaultBase, currentJournal, 'receipt-published');
775
+ }
776
+ injectFault(options.faultAt, 'after-receipt');
777
+
778
+ cleanupOperation(vaultBase, plan, options);
779
+ return {
780
+ status: 'rotated',
781
+ apply: true,
782
+ operationId: plan.operationId,
783
+ generation: plan.generation,
784
+ sourceEvents: plan.sourceEventCount,
785
+ sourceBytes: Buffer.byteLength(plan.sourceContent),
786
+ activeBytesBefore: plan.sourceActiveBytes,
787
+ activeBytesAfter: Buffer.byteLength(plan.candidateContent),
788
+ reclaimedActiveBytes: Math.max(0, plan.sourceActiveBytes - Buffer.byteLength(plan.candidateContent)),
789
+ backupFile: plan.backupFile,
790
+ backupRetained: true,
791
+ policy: MEMORY_ROTATION_POLICY,
792
+ switchStatus,
793
+ stateStatus,
794
+ snapshotStatus,
795
+ receiptStatus: receipt.status,
796
+ receiptHash: receipt.receipt.receipt_hash,
797
+ recoveryRequired: false,
798
+ };
799
+ }
800
+
801
+ function applyNewRotationLocked(vaultBase, preview, options = {}) {
802
+ const auth = authorization(options, { required: true });
803
+ if (auth.errors.length) throw new MemoryLedgerRotationBlocked(auth.errors);
804
+ const plan = {
805
+ ...preview.plan,
806
+ reason: auth.reason,
807
+ authorizedBy: auth.authorizedBy,
808
+ };
809
+ if (preview.status !== 'preview') return preview;
810
+ mkdirVaultPath(vaultBase, memoryLedgerBackupDirectory(vaultBase), {
811
+ label: 'diretório de backups das gerações do ledger',
812
+ });
813
+ const backupStatus = writeImmutableFile(
814
+ vaultBase,
815
+ backupPath(vaultBase, plan.backupFile),
816
+ plan.sourceContent,
817
+ 'backup imutável da geração do ledger',
818
+ memoryLedgerBackupDirectory(vaultBase),
819
+ );
820
+ const candidateStatus = writeImmutableFile(
821
+ vaultBase,
822
+ candidatePath(vaultBase, plan.operationId),
823
+ plan.candidateContent,
824
+ 'candidate imutável da rotação do ledger',
825
+ brainDir(vaultBase),
826
+ );
827
+ const journal = writeJournal(vaultBase, journalForPlan(plan));
828
+ injectFault(options.faultAt, 'after-prepared');
829
+ return {
830
+ ...resumeLocked(vaultBase, journal, options),
831
+ backupStatus,
832
+ candidateStatus,
833
+ };
834
+ }
835
+
836
+ export function rotateMemoryLedger(vaultBase, options = {}) {
837
+ const preview = buildPlan(vaultBase, options);
838
+ if (!options.apply) return preview;
839
+ const result = withMemoryLock(vaultBase, () => {
840
+ const pending = readMemoryRotationJournal(vaultBase);
841
+ if (pending.status === 'ok') return resumeLocked(vaultBase, pending.journal, options);
842
+ if (pending.status === 'invalid') throw new MemoryLedgerRotationBlocked(pending.errors);
843
+ const fresh = buildPlan(vaultBase, options);
844
+ if (fresh.status !== 'preview') return fresh;
845
+ return applyNewRotationLocked(vaultBase, fresh, options);
846
+ }, options.lock || {});
847
+ if (result === MEMORY_LOCK_BUSY) return { status: 'busy', apply: true };
848
+ return result;
849
+ }
850
+
851
+ function inspectOrphanCandidate(vaultBase) {
852
+ const generation = readMemoryLedgerGeneration(vaultBase);
853
+ if (generation.status === 'missing') {
854
+ return { status: 'none', apply: false, recoveryRequired: false };
855
+ }
856
+ if (generation.status !== 'ok') {
857
+ return {
858
+ status: 'blocked',
859
+ apply: false,
860
+ recoveryRequired: true,
861
+ errors: generation.errors,
862
+ };
863
+ }
864
+ const candidate = generationCandidate(vaultBase, generation.state);
865
+ if (!candidate.exists) return { status: 'none', apply: false, recoveryRequired: false };
866
+
867
+ const errors = [];
868
+ const ledger = readMemoryLedger(vaultBase);
869
+ if (ledger.status !== 'ok') errors.push(...ledger.errors.map((error) => error.message));
870
+ const receipts = readMemoryRotationReceipts(vaultBase);
871
+ if (receipts.status !== 'ok') errors.push(...receipts.errors);
872
+ if (receipts.checkpointStatus !== 'ok') errors.push(...receipts.checkpointErrors);
873
+ const raw = readMemoryControlFile(
874
+ vaultBase,
875
+ candidate.path,
876
+ 'candidate residual da rotação concluída',
877
+ { allowMissing: false },
878
+ );
879
+ if (memorySha256(raw) !== generation.state.active_ledger_hash) {
880
+ errors.push('orphan candidate hash diverges from current generation');
881
+ }
882
+ const parsed = parseMemoryLedgerContent(raw, generation.projectId, candidate.path);
883
+ if (parsed.status !== 'ok' || parsed.events.length !== 1
884
+ || parsed.events[0].event_id !== generation.state.anchor_event_id) {
885
+ errors.push('orphan candidate anchor is invalid');
886
+ }
887
+ if (errors.length) {
888
+ return {
889
+ status: 'blocked',
890
+ apply: false,
891
+ recoveryRequired: true,
892
+ operationId: generation.state.operation_id,
893
+ generation: generation.state.generation,
894
+ errors,
895
+ };
896
+ }
897
+ return {
898
+ status: 'preview',
899
+ apply: false,
900
+ recoveryRequired: true,
901
+ operationId: generation.state.operation_id,
902
+ generation: generation.state.generation,
903
+ stage: 'candidate-cleanup',
904
+ candidatePath: candidate.path,
905
+ };
906
+ }
907
+
908
+ function finalizeOrphanCandidateLocked(vaultBase) {
909
+ const inspection = inspectOrphanCandidate(vaultBase);
910
+ if (inspection.status !== 'preview') return inspection;
911
+ unlinkVaultFile(vaultBase, inspection.candidatePath, {
912
+ missingOk: false,
913
+ label: 'candidate residual da rotação concluída',
914
+ });
915
+ return {
916
+ status: 'finalized',
917
+ apply: true,
918
+ recoveryRequired: false,
919
+ operationId: inspection.operationId,
920
+ generation: inspection.generation,
921
+ stage: inspection.stage,
922
+ };
923
+ }
924
+
925
+ export function recoverMemoryLedgerRotation(vaultBase, options = {}) {
926
+ const pending = readMemoryRotationJournal(vaultBase);
927
+ if (pending.status === 'missing') {
928
+ if (!options.apply) return inspectOrphanCandidate(vaultBase);
929
+ const result = withMemoryLock(
930
+ vaultBase,
931
+ () => finalizeOrphanCandidateLocked(vaultBase),
932
+ options.lock || {},
933
+ );
934
+ if (result === MEMORY_LOCK_BUSY) return { status: 'busy', apply: true, recoveryRequired: true };
935
+ return result;
936
+ }
937
+ if (pending.status !== 'ok') {
938
+ return { status: 'blocked', apply: false, recoveryRequired: true, errors: pending.errors };
939
+ }
940
+ if (!options.apply) {
941
+ return {
942
+ status: 'preview',
943
+ apply: false,
944
+ recoveryRequired: true,
945
+ operationId: pending.journal.operation_id,
946
+ stage: pending.journal.stage,
947
+ generation: pending.journal.plan.generation,
948
+ };
949
+ }
950
+ const result = withMemoryLock(
951
+ vaultBase,
952
+ () => resumeLocked(vaultBase, readMemoryRotationJournal(vaultBase).journal, options),
953
+ options.lock || {},
954
+ );
955
+ if (result === MEMORY_LOCK_BUSY) return { status: 'busy', apply: true, recoveryRequired: true };
956
+ return result;
957
+ }
958
+
959
+ export function memoryLedgerRotationStatus(vaultBase) {
960
+ return {
961
+ ...memoryLedgerGenerationStatus(vaultBase),
962
+ journalDetails: readMemoryRotationJournal(vaultBase),
963
+ orphanCandidate: inspectOrphanCandidate(vaultBase),
964
+ };
965
+ }
966
+
967
+ export const compactMemoryLedger = rotateMemoryLedger;