wendkeep 0.85.0 → 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.
- package/CHANGELOG.md +40 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/en/commands/evidence-embeddings.md +243 -0
- package/docs/en/commands/mcp.md +67 -7
- package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
- package/docs/pt-BR/commands/mcp.md +66 -7
- package/hooks/evidence-context.mjs +41 -7
- package/hooks/evidence-recall.mjs +10 -0
- package/package.json +1 -1
- package/packages/mcp/src/effects.mjs +3 -2
- package/packages/mcp/src/evidence-recall.mjs +130 -0
- package/packages/mcp/src/executor.mjs +4 -0
- package/packages/mcp/src/server.mjs +31 -1
- package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
- package/packages/vault/src/evidence-index-store.mjs +360 -0
- package/packages/vault/src/evidence-recall-page.mjs +381 -0
- package/packages/vault/src/evidence-search-index.mjs +917 -0
- package/packages/vault/src/index.mjs +12 -1
- package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
- package/packages/vault/src/memory-ledger-view.mjs +41 -0
- package/packages/vault/src/memory-rotation-store.mjs +967 -0
- package/packages/vault/src/memory-segment-store.mjs +820 -0
- package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
- package/packages/vault/src/memory-store-base.mjs +1161 -0
- package/packages/vault/src/memory-store-core.mjs +2 -0
- package/packages/vault/src/memory-store.mjs +46 -1161
- package/src/doctor.mjs +41 -5
- package/src/evidence-search-health.mjs +221 -0
- package/src/memory-scale-health.mjs +210 -0
- package/src/observer-snapshot.mjs +87 -1
|
@@ -8,6 +8,17 @@ export * from './memory-handoff.mjs';
|
|
|
8
8
|
export * from './memory-store.mjs';
|
|
9
9
|
export * from './memory-scope.mjs';
|
|
10
10
|
export * from './evidence-recall.mjs';
|
|
11
|
+
export * from './evidence-recall-page.mjs';
|
|
12
|
+
export * from './evidence-search-index.mjs';
|
|
13
|
+
export * from './evidence-embedding-plugin.mjs';
|
|
14
|
+
export {
|
|
15
|
+
EVIDENCE_INDEX_STATE_FILE,
|
|
16
|
+
EVIDENCE_INDEX_STATE_VERSION,
|
|
17
|
+
buildIncrementalEvidenceIndex,
|
|
18
|
+
buildIncrementalEvidenceIndex as buildEvidenceIndex,
|
|
19
|
+
loadEvidenceIndexState,
|
|
20
|
+
refreshEvidenceIndex,
|
|
21
|
+
} from './evidence-index-store.mjs';
|
|
11
22
|
export * from './evidence-envelope.mjs';
|
|
12
23
|
export * from './validate-core.mjs';
|
|
13
|
-
export * from './validate-memory.mjs';
|
|
24
|
+
export * from './validate-memory.mjs';
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
canonicalMemoryJson,
|
|
7
|
+
readMemoryLedger as readActiveMemoryLedger,
|
|
8
|
+
} from './memory-store-base.mjs';
|
|
9
|
+
import { validateMemoryEvent } from './memory-schema.mjs';
|
|
10
|
+
import { assertVaultPathSafe } from './vault-path-safety.mjs';
|
|
11
|
+
|
|
12
|
+
export const MEMORY_LEDGER_GENERATION_FILE = 'MEMORY_LEDGER_GENERATION.json';
|
|
13
|
+
export const MEMORY_LEDGER_GENERATION_SCHEMA_VERSION = 1;
|
|
14
|
+
export const MEMORY_LEDGER_BACKUP_DIRECTORY = 'memory-ledger-generations';
|
|
15
|
+
export const MEMORY_ROTATION_JOURNAL_FILE = 'MEMORY_ROTATION_JOURNAL.json';
|
|
16
|
+
export const MEMORY_ROTATION_JOURNAL_SCHEMA_VERSION = 1;
|
|
17
|
+
export const MEMORY_ROTATION_RECEIPTS_FILE = 'MEMORY_ROTATION_RECEIPTS.jsonl';
|
|
18
|
+
export const MEMORY_ROTATION_RECEIPT_CHECKPOINT_FILE = 'MEMORY_ROTATION_RECEIPTS.checkpoint.json';
|
|
19
|
+
export const MEMORY_ROTATION_RECEIPT_SCHEMA_VERSION = 1;
|
|
20
|
+
export const MEMORY_ROTATION_RECEIPT_CHECKPOINT_SCHEMA_VERSION = 1;
|
|
21
|
+
|
|
22
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
23
|
+
const OPERATION_ID = /^memrot-[a-f0-9]{16}$/;
|
|
24
|
+
const EVENT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
25
|
+
const BACKUP_FILE = /^memory-ledger-generations\/ledger-gen-(\d{6})-([a-f0-9]{16})\.jsonl$/;
|
|
26
|
+
const RECEIPT_KIND = 'memory-ledger-rotation';
|
|
27
|
+
const CHAIN_GENESIS = '0'.repeat(64);
|
|
28
|
+
|
|
29
|
+
export class MemoryLedgerGenerationCorruption extends Error {
|
|
30
|
+
constructor(errors, message = 'Memory ledger generation is corrupt or incomplete; recover the rotation before continuing.') {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'MemoryLedgerGenerationCorruption';
|
|
33
|
+
this.code = 'MEMORY_LEDGER_GENERATION_CORRUPT';
|
|
34
|
+
this.errors = Array.isArray(errors) ? errors : [String(errors || 'unknown generation error')];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class MemoryRotationReceiptCorruption extends Error {
|
|
39
|
+
constructor(errors, message = 'Memory rotation receipt chain is corrupt or incomplete.') {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'MemoryRotationReceiptCorruption';
|
|
42
|
+
this.code = 'MEMORY_ROTATION_RECEIPT_CORRUPT';
|
|
43
|
+
this.errors = Array.isArray(errors) ? errors : [String(errors || 'unknown receipt error')];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function brainDir(vaultBase) { return join(vaultBase, '.brain'); }
|
|
48
|
+
export function memoryLedgerPath(vaultBase) { return join(brainDir(vaultBase), 'MEMORY_EVENTS.jsonl'); }
|
|
49
|
+
export function memoryLedgerGenerationPath(vaultBase) {
|
|
50
|
+
return join(brainDir(vaultBase), MEMORY_LEDGER_GENERATION_FILE);
|
|
51
|
+
}
|
|
52
|
+
export function memoryLedgerBackupDirectory(vaultBase) {
|
|
53
|
+
return join(brainDir(vaultBase), MEMORY_LEDGER_BACKUP_DIRECTORY);
|
|
54
|
+
}
|
|
55
|
+
export function memoryRotationJournalPath(vaultBase) {
|
|
56
|
+
return join(brainDir(vaultBase), MEMORY_ROTATION_JOURNAL_FILE);
|
|
57
|
+
}
|
|
58
|
+
export function memoryRotationReceiptsPath(vaultBase) {
|
|
59
|
+
return join(brainDir(vaultBase), MEMORY_ROTATION_RECEIPTS_FILE);
|
|
60
|
+
}
|
|
61
|
+
export function memoryRotationReceiptCheckpointPath(vaultBase) {
|
|
62
|
+
return join(brainDir(vaultBase), MEMORY_ROTATION_RECEIPT_CHECKPOINT_FILE);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function memorySha256(value) {
|
|
66
|
+
return createHash('sha256').update(value).digest('hex');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function canonicalMemoryControlJson(value) {
|
|
70
|
+
const canonicalize = (item) => {
|
|
71
|
+
if (Array.isArray(item)) return item.map(canonicalize);
|
|
72
|
+
if (item && typeof item === 'object') {
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const key of Object.keys(item).sort()) {
|
|
75
|
+
if (item[key] !== undefined) out[key] = canonicalize(item[key]);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
return item;
|
|
80
|
+
};
|
|
81
|
+
return JSON.stringify(canonicalize(value));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function hashMemoryControl(value, excludedKey = '') {
|
|
85
|
+
const copy = { ...(value || {}) };
|
|
86
|
+
if (excludedKey) delete copy[excludedKey];
|
|
87
|
+
return memorySha256(canonicalMemoryControlJson(copy));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkedFile(vaultBase, path, label, { allowMissing = true } = {}) {
|
|
91
|
+
return assertVaultPathSafe(vaultBase, path, {
|
|
92
|
+
allowMissing,
|
|
93
|
+
expectedType: 'file',
|
|
94
|
+
label,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function readMemoryControlFile(vaultBase, path, label, { allowMissing = true } = {}) {
|
|
99
|
+
let checked = checkedFile(vaultBase, path, label, { allowMissing });
|
|
100
|
+
if (!checked.exists) return null;
|
|
101
|
+
checked = checkedFile(vaultBase, checked.target, label, { allowMissing: false });
|
|
102
|
+
return readFileSync(checked.target, 'utf8');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function projectIdForMemoryLedger(vaultBase) {
|
|
106
|
+
const raw = readMemoryControlFile(
|
|
107
|
+
vaultBase,
|
|
108
|
+
join(brainDir(vaultBase), 'PROJECT.json'),
|
|
109
|
+
'autoridade PROJECT.json da geração do ledger',
|
|
110
|
+
{ allowMissing: true },
|
|
111
|
+
);
|
|
112
|
+
if (raw === null) return '';
|
|
113
|
+
try {
|
|
114
|
+
const project = JSON.parse(raw);
|
|
115
|
+
return typeof project.projectId === 'string' ? project.projectId : '';
|
|
116
|
+
} catch {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ledgerError(line, message, partial = false) {
|
|
122
|
+
return { line, message, partial };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseMemoryLedgerContent(rawInput, projectId = '', path = '') {
|
|
126
|
+
const raw = String(rawInput || '').replace(/\r\n/g, '\n');
|
|
127
|
+
const lines = raw.split('\n');
|
|
128
|
+
const hasPartialTail = raw.length > 0 && !raw.endsWith('\n');
|
|
129
|
+
if (!hasPartialTail) lines.pop();
|
|
130
|
+
const events = [];
|
|
131
|
+
const eventIds = new Set();
|
|
132
|
+
const eventPayloads = new Map();
|
|
133
|
+
const errors = [];
|
|
134
|
+
|
|
135
|
+
lines.forEach((line, index) => {
|
|
136
|
+
const lineNumber = index + 1;
|
|
137
|
+
const partial = hasPartialTail && index === lines.length - 1;
|
|
138
|
+
if (!line.trim()) {
|
|
139
|
+
errors.push(ledgerError(lineNumber, 'blank ledger line', partial));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(line);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
errors.push(ledgerError(lineNumber, `invalid JSON: ${error.message}`, partial));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const validation = validateMemoryEvent(parsed, projectId ? { projectId } : {});
|
|
150
|
+
if (!validation.ok) {
|
|
151
|
+
errors.push(ledgerError(lineNumber, validation.errors.join(' '), partial));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const payload = canonicalMemoryJson(parsed);
|
|
155
|
+
if (eventIds.has(parsed.event_id)) {
|
|
156
|
+
errors.push(ledgerError(
|
|
157
|
+
lineNumber,
|
|
158
|
+
eventPayloads.get(parsed.event_id) === payload
|
|
159
|
+
? `duplicate event_id: ${parsed.event_id}`
|
|
160
|
+
: `event_id collision: ${parsed.event_id}`,
|
|
161
|
+
partial,
|
|
162
|
+
));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
eventIds.add(parsed.event_id);
|
|
166
|
+
eventPayloads.set(parsed.event_id, payload);
|
|
167
|
+
events.push(parsed);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
status: errors.length ? 'corrupt' : 'ok',
|
|
172
|
+
path,
|
|
173
|
+
raw,
|
|
174
|
+
events,
|
|
175
|
+
eventIds,
|
|
176
|
+
eventPayloads,
|
|
177
|
+
errors,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function readMemoryLedgerFile(vaultBase, path, label, { allowMissing = true } = {}) {
|
|
182
|
+
const raw = readMemoryControlFile(vaultBase, path, label, { allowMissing });
|
|
183
|
+
if (raw === null) {
|
|
184
|
+
return {
|
|
185
|
+
status: 'ok', path, raw: '', events: [], eventIds: new Set(), eventPayloads: new Map(), errors: [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return parseMemoryLedgerContent(raw, projectIdForMemoryLedger(vaultBase), path);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function generationErrors(state, projectId) {
|
|
192
|
+
const errors = [];
|
|
193
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) return ['generation state is not an object'];
|
|
194
|
+
if (state.schema_version !== MEMORY_LEDGER_GENERATION_SCHEMA_VERSION) errors.push('generation schema_version mismatch');
|
|
195
|
+
if (state.project_id !== projectId) errors.push('generation project_id mismatch');
|
|
196
|
+
if (!Number.isInteger(state.generation) || state.generation < 1) errors.push('generation number invalid');
|
|
197
|
+
if (!Number.isInteger(state.previous_generation) || state.previous_generation !== state.generation - 1) {
|
|
198
|
+
errors.push('previous_generation mismatch');
|
|
199
|
+
}
|
|
200
|
+
if (!OPERATION_ID.test(String(state.operation_id || ''))) errors.push('operation_id invalid');
|
|
201
|
+
if (!BACKUP_FILE.test(String(state.backup_file || ''))) errors.push('backup_file invalid');
|
|
202
|
+
if (!SHA256.test(String(state.backup_hash || ''))) errors.push('backup_hash invalid');
|
|
203
|
+
if (!SHA256.test(String(state.source_ledger_hash || ''))) errors.push('source_ledger_hash invalid');
|
|
204
|
+
if (!Number.isInteger(state.source_event_count) || state.source_event_count < 1) errors.push('source_event_count invalid');
|
|
205
|
+
if (!EVENT_ID.test(String(state.anchor_event_id || ''))) errors.push('anchor_event_id invalid');
|
|
206
|
+
if (!SHA256.test(String(state.anchor_payload_hash || ''))) errors.push('anchor_payload_hash invalid');
|
|
207
|
+
if (!SHA256.test(String(state.active_ledger_hash || ''))) errors.push('active_ledger_hash invalid');
|
|
208
|
+
if (!SHA256.test(String(state.segment_manifest_hash || ''))) errors.push('segment_manifest_hash invalid');
|
|
209
|
+
if (!SHA256.test(String(state.segment_chain_tip || ''))) errors.push('segment_chain_tip invalid');
|
|
210
|
+
if (!SHA256.test(String(state.snapshot_hash || ''))) errors.push('snapshot_hash invalid');
|
|
211
|
+
if (!SHA256.test(String(state.previous_state_hash || ''))) errors.push('previous_state_hash invalid');
|
|
212
|
+
if (typeof state.rotated_at !== 'string' || Number.isNaN(Date.parse(state.rotated_at))) errors.push('rotated_at invalid');
|
|
213
|
+
if (!SHA256.test(String(state.state_hash || '')) || state.state_hash !== hashMemoryControl(state, 'state_hash')) {
|
|
214
|
+
errors.push('generation state_hash mismatch');
|
|
215
|
+
}
|
|
216
|
+
const backup = BACKUP_FILE.exec(String(state.backup_file || ''));
|
|
217
|
+
if (backup && Number(backup[1]) !== state.generation) errors.push('backup generation mismatch');
|
|
218
|
+
return errors;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function readMemoryLedgerGeneration(vaultBase) {
|
|
222
|
+
const projectId = projectIdForMemoryLedger(vaultBase);
|
|
223
|
+
const raw = readMemoryControlFile(
|
|
224
|
+
vaultBase,
|
|
225
|
+
memoryLedgerGenerationPath(vaultBase),
|
|
226
|
+
'estado da geração do ledger de memória',
|
|
227
|
+
{ allowMissing: true },
|
|
228
|
+
);
|
|
229
|
+
if (raw === null) return { status: 'missing', projectId, state: null, raw: null, errors: [] };
|
|
230
|
+
let state;
|
|
231
|
+
try { state = JSON.parse(raw); } catch { return { status: 'invalid', projectId, state: null, raw, errors: ['generation invalid JSON'] }; }
|
|
232
|
+
const errors = generationErrors(state, projectId);
|
|
233
|
+
return errors.length
|
|
234
|
+
? { status: 'invalid', projectId, state, raw, errors }
|
|
235
|
+
: { status: 'ok', projectId, state, raw, errors: [] };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function journalErrors(journal, projectId) {
|
|
239
|
+
const errors = [];
|
|
240
|
+
if (!journal || typeof journal !== 'object' || Array.isArray(journal)) return ['rotation journal is not an object'];
|
|
241
|
+
if (journal.schema_version !== MEMORY_ROTATION_JOURNAL_SCHEMA_VERSION) errors.push('journal schema_version mismatch');
|
|
242
|
+
if (journal.project_id !== projectId) errors.push('journal project_id mismatch');
|
|
243
|
+
if (!OPERATION_ID.test(String(journal.operation_id || ''))) errors.push('journal operation_id invalid');
|
|
244
|
+
if (!['prepared', 'switched', 'state-published', 'snapshot-published', 'receipt-published'].includes(journal.stage)) {
|
|
245
|
+
errors.push('journal stage invalid');
|
|
246
|
+
}
|
|
247
|
+
if (!journal.plan || typeof journal.plan !== 'object' || Array.isArray(journal.plan)) errors.push('journal plan missing');
|
|
248
|
+
if (typeof journal.created_at !== 'string' || Number.isNaN(Date.parse(journal.created_at))) errors.push('journal created_at invalid');
|
|
249
|
+
if (typeof journal.updated_at !== 'string' || Number.isNaN(Date.parse(journal.updated_at))) errors.push('journal updated_at invalid');
|
|
250
|
+
if (!SHA256.test(String(journal.journal_hash || '')) || journal.journal_hash !== hashMemoryControl(journal, 'journal_hash')) {
|
|
251
|
+
errors.push('journal_hash mismatch');
|
|
252
|
+
}
|
|
253
|
+
return errors;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function readMemoryRotationJournal(vaultBase) {
|
|
257
|
+
const projectId = projectIdForMemoryLedger(vaultBase);
|
|
258
|
+
const raw = readMemoryControlFile(
|
|
259
|
+
vaultBase,
|
|
260
|
+
memoryRotationJournalPath(vaultBase),
|
|
261
|
+
'journal da rotação do ledger de memória',
|
|
262
|
+
{ allowMissing: true },
|
|
263
|
+
);
|
|
264
|
+
if (raw === null) return { status: 'missing', projectId, journal: null, raw: null, errors: [] };
|
|
265
|
+
let journal;
|
|
266
|
+
try { journal = JSON.parse(raw); } catch { return { status: 'invalid', projectId, journal: null, raw, errors: ['journal invalid JSON'] }; }
|
|
267
|
+
const errors = journalErrors(journal, projectId);
|
|
268
|
+
return errors.length
|
|
269
|
+
? { status: 'invalid', projectId, journal, raw, errors }
|
|
270
|
+
: { status: 'ok', projectId, journal, raw, errors: [] };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function receiptHash(receipt) {
|
|
274
|
+
return hashMemoryControl(receipt, 'receipt_hash');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function checkpointHash(checkpoint) {
|
|
278
|
+
return hashMemoryControl(checkpoint, 'checkpoint_hash');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function parseMemoryRotationReceipts(rawInput, projectId = '') {
|
|
282
|
+
const raw = String(rawInput || '').replace(/\r\n/g, '\n');
|
|
283
|
+
const lines = raw.split('\n');
|
|
284
|
+
const partial = raw.length > 0 && !raw.endsWith('\n');
|
|
285
|
+
if (!partial) lines.pop();
|
|
286
|
+
const receipts = [];
|
|
287
|
+
const errors = [];
|
|
288
|
+
let previousHash = CHAIN_GENESIS;
|
|
289
|
+
const operations = new Set();
|
|
290
|
+
|
|
291
|
+
lines.forEach((line, index) => {
|
|
292
|
+
const lineNumber = index + 1;
|
|
293
|
+
if (!line.trim()) {
|
|
294
|
+
errors.push(`receipt line ${lineNumber} blank${partial && index === lines.length - 1 ? ' (partial)' : ''}`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
let receipt;
|
|
298
|
+
try { receipt = JSON.parse(line); } catch {
|
|
299
|
+
errors.push(`receipt line ${lineNumber} invalid JSON${partial && index === lines.length - 1 ? ' (partial)' : ''}`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (receipt.schema_version !== MEMORY_ROTATION_RECEIPT_SCHEMA_VERSION
|
|
303
|
+
|| receipt.kind !== RECEIPT_KIND
|
|
304
|
+
|| receipt.project_id !== projectId
|
|
305
|
+
|| receipt.sequence !== lineNumber
|
|
306
|
+
|| !OPERATION_ID.test(String(receipt.operation_id || ''))
|
|
307
|
+
|| operations.has(receipt.operation_id)
|
|
308
|
+
|| receipt.previous_receipt_hash !== previousHash
|
|
309
|
+
|| !SHA256.test(String(receipt.receipt_hash || ''))
|
|
310
|
+
|| receipt.receipt_hash !== receiptHash(receipt)
|
|
311
|
+
|| !Number.isInteger(receipt.generation) || receipt.generation < 1
|
|
312
|
+
|| !SHA256.test(String(receipt.generation_state_hash || ''))
|
|
313
|
+
|| !SHA256.test(String(receipt.backup_hash || ''))
|
|
314
|
+
|| !SHA256.test(String(receipt.source_ledger_hash || ''))
|
|
315
|
+
|| !SHA256.test(String(receipt.segment_manifest_hash || ''))
|
|
316
|
+
|| !SHA256.test(String(receipt.snapshot_hash || ''))
|
|
317
|
+
|| typeof receipt.reason !== 'string' || receipt.reason.trim().length < 3
|
|
318
|
+
|| typeof receipt.authorized_by !== 'string' || receipt.authorized_by.trim().length < 2
|
|
319
|
+
|| typeof receipt.completed_at !== 'string' || Number.isNaN(Date.parse(receipt.completed_at))) {
|
|
320
|
+
errors.push(`receipt line ${lineNumber} shape/hash mismatch`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
operations.add(receipt.operation_id);
|
|
324
|
+
previousHash = receipt.receipt_hash;
|
|
325
|
+
receipts.push(receipt);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
status: errors.length ? 'corrupt' : 'ok',
|
|
330
|
+
raw,
|
|
331
|
+
receipts,
|
|
332
|
+
errors,
|
|
333
|
+
lastHash: previousHash,
|
|
334
|
+
operations,
|
|
335
|
+
partial,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function readReceiptCheckpoint(vaultBase, receiptRaw, parsed) {
|
|
340
|
+
const raw = readMemoryControlFile(
|
|
341
|
+
vaultBase,
|
|
342
|
+
memoryRotationReceiptCheckpointPath(vaultBase),
|
|
343
|
+
'checkpoint dos receipts de rotação',
|
|
344
|
+
{ allowMissing: true },
|
|
345
|
+
);
|
|
346
|
+
if (raw === null) {
|
|
347
|
+
return parsed.receipts.length === 0
|
|
348
|
+
? { status: 'empty', checkpoint: null, raw: null, errors: [] }
|
|
349
|
+
: { status: 'missing', checkpoint: null, raw: null, errors: ['receipt checkpoint missing'] };
|
|
350
|
+
}
|
|
351
|
+
let checkpoint;
|
|
352
|
+
try { checkpoint = JSON.parse(raw); } catch {
|
|
353
|
+
return { status: 'invalid', checkpoint: null, raw, errors: ['receipt checkpoint invalid JSON'] };
|
|
354
|
+
}
|
|
355
|
+
const errors = [];
|
|
356
|
+
if (checkpoint.schema_version !== MEMORY_ROTATION_RECEIPT_CHECKPOINT_SCHEMA_VERSION) errors.push('receipt checkpoint schema mismatch');
|
|
357
|
+
if (checkpoint.project_id !== projectIdForMemoryLedger(vaultBase)) errors.push('receipt checkpoint project mismatch');
|
|
358
|
+
if (checkpoint.count !== parsed.receipts.length) errors.push('receipt checkpoint count mismatch');
|
|
359
|
+
if (checkpoint.last_hash !== parsed.lastHash) errors.push('receipt checkpoint last_hash mismatch');
|
|
360
|
+
if (checkpoint.file_bytes !== Buffer.byteLength(receiptRaw)) errors.push('receipt checkpoint file_bytes mismatch');
|
|
361
|
+
if (!SHA256.test(String(checkpoint.checkpoint_hash || ''))
|
|
362
|
+
|| checkpoint.checkpoint_hash !== checkpointHash(checkpoint)) errors.push('receipt checkpoint hash mismatch');
|
|
363
|
+
return errors.length
|
|
364
|
+
? { status: 'invalid', checkpoint, raw, errors }
|
|
365
|
+
: { status: 'ok', checkpoint, raw, errors: [] };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function readMemoryRotationReceipts(vaultBase) {
|
|
369
|
+
const path = memoryRotationReceiptsPath(vaultBase);
|
|
370
|
+
const raw = readMemoryControlFile(
|
|
371
|
+
vaultBase,
|
|
372
|
+
path,
|
|
373
|
+
'ledger de receipts da rotação de memória',
|
|
374
|
+
{ allowMissing: true },
|
|
375
|
+
) ?? '';
|
|
376
|
+
const parsed = parseMemoryRotationReceipts(raw, projectIdForMemoryLedger(vaultBase));
|
|
377
|
+
const checkpoint = parsed.status === 'ok'
|
|
378
|
+
? readReceiptCheckpoint(vaultBase, raw, parsed)
|
|
379
|
+
: { status: 'unavailable', checkpoint: null, raw: null, errors: [] };
|
|
380
|
+
return {
|
|
381
|
+
...parsed,
|
|
382
|
+
path,
|
|
383
|
+
checkpointStatus: checkpoint.status,
|
|
384
|
+
checkpoint: checkpoint.checkpoint,
|
|
385
|
+
checkpointErrors: checkpoint.errors,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function backupPath(vaultBase, relativePath) {
|
|
390
|
+
if (!BACKUP_FILE.test(String(relativePath || '')) || String(relativePath).includes('..') || String(relativePath).includes('\\')) {
|
|
391
|
+
throw new MemoryLedgerGenerationCorruption([`unsafe backup path: ${relativePath}`]);
|
|
392
|
+
}
|
|
393
|
+
return join(brainDir(vaultBase), ...String(relativePath).split('/'));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function receiptForGeneration(receipts, state) {
|
|
397
|
+
return receipts.receipts.find((receipt) => (
|
|
398
|
+
receipt.operation_id === state.operation_id
|
|
399
|
+
&& receipt.generation === state.generation
|
|
400
|
+
&& receipt.generation_state_hash === state.state_hash
|
|
401
|
+
&& receipt.backup_hash === state.backup_hash
|
|
402
|
+
&& receipt.source_ledger_hash === state.source_ledger_hash
|
|
403
|
+
&& receipt.segment_manifest_hash === state.segment_manifest_hash
|
|
404
|
+
&& receipt.snapshot_hash === state.snapshot_hash
|
|
405
|
+
));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function generationFailure(vaultBase, errors, active = null, generation = null) {
|
|
409
|
+
return {
|
|
410
|
+
status: 'corrupt',
|
|
411
|
+
path: memoryLedgerPath(vaultBase),
|
|
412
|
+
raw: active?.raw || '',
|
|
413
|
+
events: active?.events || [],
|
|
414
|
+
eventIds: active?.eventIds || new Set(),
|
|
415
|
+
errors: errors.map((message, index) => ledgerError(index + 1, message, false)),
|
|
416
|
+
generation,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Return the canonical logical ledger. Before the first rotation it is the active JSONL.
|
|
422
|
+
* After rotation it is the immutable full-generation backup plus the active anchor/tail.
|
|
423
|
+
* Any unfinished journal blocks reads so a truncated active generation is never mistaken for
|
|
424
|
+
* the complete authority.
|
|
425
|
+
*/
|
|
426
|
+
export function readMemoryLedger(vaultBase) {
|
|
427
|
+
const journal = readMemoryRotationJournal(vaultBase);
|
|
428
|
+
if (journal.status === 'ok') {
|
|
429
|
+
return generationFailure(vaultBase, [
|
|
430
|
+
`memory rotation recovery required for ${journal.journal.operation_id} at stage ${journal.journal.stage}`,
|
|
431
|
+
]);
|
|
432
|
+
}
|
|
433
|
+
if (journal.status === 'invalid') return generationFailure(vaultBase, journal.errors);
|
|
434
|
+
|
|
435
|
+
const generation = readMemoryLedgerGeneration(vaultBase);
|
|
436
|
+
if (generation.status === 'missing') return readActiveMemoryLedger(vaultBase);
|
|
437
|
+
if (generation.status !== 'ok') return generationFailure(vaultBase, generation.errors, null, generation.state);
|
|
438
|
+
|
|
439
|
+
const receipts = readMemoryRotationReceipts(vaultBase);
|
|
440
|
+
if (receipts.status !== 'ok' || !['ok', 'empty'].includes(receipts.checkpointStatus)) {
|
|
441
|
+
return generationFailure(vaultBase, [
|
|
442
|
+
...receipts.errors,
|
|
443
|
+
...receipts.checkpointErrors,
|
|
444
|
+
], null, generation.state);
|
|
445
|
+
}
|
|
446
|
+
if (!receiptForGeneration(receipts, generation.state)) {
|
|
447
|
+
return generationFailure(vaultBase, ['rotation receipt does not authorize current generation'], null, generation.state);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let backup;
|
|
451
|
+
try {
|
|
452
|
+
const path = backupPath(vaultBase, generation.state.backup_file);
|
|
453
|
+
const raw = readMemoryControlFile(vaultBase, path, 'backup imutável da geração do ledger', {
|
|
454
|
+
allowMissing: false,
|
|
455
|
+
});
|
|
456
|
+
if (memorySha256(raw) !== generation.state.backup_hash
|
|
457
|
+
|| memorySha256(raw) !== generation.state.source_ledger_hash) {
|
|
458
|
+
return generationFailure(vaultBase, ['generation backup hash mismatch'], null, generation.state);
|
|
459
|
+
}
|
|
460
|
+
backup = parseMemoryLedgerContent(raw, generation.projectId, path);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
463
|
+
return generationFailure(vaultBase, ['generation backup unreadable'], null, generation.state);
|
|
464
|
+
}
|
|
465
|
+
if (backup.status !== 'ok' || backup.events.length !== generation.state.source_event_count) {
|
|
466
|
+
return generationFailure(vaultBase, [
|
|
467
|
+
...backup.errors.map((error) => `backup line ${error.line}: ${error.message}`),
|
|
468
|
+
'generation backup event count mismatch',
|
|
469
|
+
], null, generation.state);
|
|
470
|
+
}
|
|
471
|
+
const anchor = backup.events.at(-1);
|
|
472
|
+
if (!anchor
|
|
473
|
+
|| anchor.event_id !== generation.state.anchor_event_id
|
|
474
|
+
|| memorySha256(canonicalMemoryJson(anchor)) !== generation.state.anchor_payload_hash) {
|
|
475
|
+
return generationFailure(vaultBase, ['generation backup anchor mismatch'], null, generation.state);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const active = readActiveMemoryLedger(vaultBase);
|
|
479
|
+
if (active.status !== 'ok') return generationFailure(vaultBase, active.errors.map((error) => error.message), active, generation.state);
|
|
480
|
+
if (!active.events.length) return generationFailure(vaultBase, ['active generation is missing its anchor event'], active, generation.state);
|
|
481
|
+
const activeAnchor = active.events[0];
|
|
482
|
+
if (canonicalMemoryJson(activeAnchor) !== canonicalMemoryJson(anchor)
|
|
483
|
+
|| memorySha256(`${canonicalMemoryJson(activeAnchor)}\n`) !== generation.state.active_ledger_hash) {
|
|
484
|
+
return generationFailure(vaultBase, ['active generation anchor mismatch'], active, generation.state);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const events = [...backup.events];
|
|
488
|
+
const eventIds = new Set(backup.eventIds);
|
|
489
|
+
const errors = [];
|
|
490
|
+
for (let index = 1; index < active.events.length; index += 1) {
|
|
491
|
+
const event = active.events[index];
|
|
492
|
+
if (eventIds.has(event.event_id)) {
|
|
493
|
+
const previous = backup.eventPayloads.get(event.event_id);
|
|
494
|
+
errors.push(ledgerError(
|
|
495
|
+
generation.state.source_event_count + index,
|
|
496
|
+
previous === canonicalMemoryJson(event)
|
|
497
|
+
? `duplicate event_id across generation boundary: ${event.event_id}`
|
|
498
|
+
: `event_id collision across generation boundary: ${event.event_id}`,
|
|
499
|
+
));
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
eventIds.add(event.event_id);
|
|
503
|
+
events.push(event);
|
|
504
|
+
}
|
|
505
|
+
if (errors.length) return generationFailure(vaultBase, errors.map((error) => error.message), active, generation.state);
|
|
506
|
+
|
|
507
|
+
const tail = active.events.slice(1);
|
|
508
|
+
const tailRaw = tail.map((event) => canonicalMemoryJson(event)).join('\n') + (tail.length ? '\n' : '');
|
|
509
|
+
return {
|
|
510
|
+
status: 'ok',
|
|
511
|
+
path: active.path,
|
|
512
|
+
raw: `${backup.raw}${tailRaw}`,
|
|
513
|
+
events,
|
|
514
|
+
eventIds,
|
|
515
|
+
errors: [],
|
|
516
|
+
generation: generation.state,
|
|
517
|
+
backupPath: backup.path,
|
|
518
|
+
activeRaw: active.raw,
|
|
519
|
+
activeEvents: active.events,
|
|
520
|
+
activeTailEvents: tail,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function memoryLedgerGenerationStatus(vaultBase) {
|
|
525
|
+
const generation = readMemoryLedgerGeneration(vaultBase);
|
|
526
|
+
const journal = readMemoryRotationJournal(vaultBase);
|
|
527
|
+
const receipts = readMemoryRotationReceipts(vaultBase);
|
|
528
|
+
const ledger = readMemoryLedger(vaultBase);
|
|
529
|
+
let backupBytes = 0;
|
|
530
|
+
if (generation.status === 'ok') {
|
|
531
|
+
try { backupBytes = Number(statSync(backupPath(vaultBase, generation.state.backup_file)).size || 0); } catch { /* reported by ledger */ }
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
status: ledger.status,
|
|
535
|
+
generation: generation.status === 'ok' ? generation.state.generation : 0,
|
|
536
|
+
operationId: generation.status === 'ok' ? generation.state.operation_id : null,
|
|
537
|
+
sourceEvents: generation.status === 'ok' ? generation.state.source_event_count : 0,
|
|
538
|
+
activeTailEvents: ledger.status === 'ok' ? Number(ledger.activeTailEvents?.length || 0) : 0,
|
|
539
|
+
backupBytes,
|
|
540
|
+
journal: journal.status === 'ok' ? journal.journal.stage : journal.status,
|
|
541
|
+
receipts: receipts.receipts.length,
|
|
542
|
+
receiptCheckpoint: receipts.checkpointStatus,
|
|
543
|
+
errors: ledger.errors.map((error) => error.message),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readMemoryLedgerGeneration as readStoredMemoryLedgerGeneration,
|
|
3
|
+
readMemoryRotationJournal,
|
|
4
|
+
} from './memory-ledger-view-base.mjs';
|
|
5
|
+
|
|
6
|
+
export * from './memory-ledger-view-base.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Expose the persisted generation normally, but model the single atomic transition where
|
|
10
|
+
* a switched active ledger still points at the previous generation state as "not published".
|
|
11
|
+
* The rotation writer can then replace that previous state after proving the journal's
|
|
12
|
+
* previous_generation and previous_state_hash bindings. Other stages retain the persisted view.
|
|
13
|
+
*/
|
|
14
|
+
export function readMemoryLedgerGeneration(vaultBase) {
|
|
15
|
+
const current = readStoredMemoryLedgerGeneration(vaultBase);
|
|
16
|
+
if (current.status !== 'ok') return current;
|
|
17
|
+
|
|
18
|
+
const journal = readMemoryRotationJournal(vaultBase);
|
|
19
|
+
const plan = journal.journal?.plan;
|
|
20
|
+
const transitioning = journal.status === 'ok'
|
|
21
|
+
&& journal.journal.stage === 'switched'
|
|
22
|
+
&& Number(plan?.generation) === current.state.generation + 1
|
|
23
|
+
&& Number(plan?.previous_generation) === current.state.generation
|
|
24
|
+
&& plan?.previous_state_hash === current.state.state_hash
|
|
25
|
+
&& plan?.generation_state?.previous_state_hash === current.state.state_hash
|
|
26
|
+
&& Number(plan?.generation_state?.previous_generation) === current.state.generation;
|
|
27
|
+
|
|
28
|
+
if (!transitioning) return current;
|
|
29
|
+
return {
|
|
30
|
+
status: 'missing',
|
|
31
|
+
projectId: current.projectId,
|
|
32
|
+
state: null,
|
|
33
|
+
raw: null,
|
|
34
|
+
errors: [],
|
|
35
|
+
transition: {
|
|
36
|
+
operationId: journal.journal.operation_id,
|
|
37
|
+
fromGeneration: current.state.generation,
|
|
38
|
+
toGeneration: plan.generation,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|