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
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
closeSync, fstatSync, fsyncSync, openSync, readFileSync, readdirSync, statSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
} from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
MEMORY_LOCK_BUSY,
|
|
10
|
+
MemoryLedgerCorruption,
|
|
11
|
+
canonicalMemoryJson,
|
|
12
|
+
memoryFileIdentityMatches,
|
|
13
|
+
readMemoryLedger,
|
|
14
|
+
withMemoryLock,
|
|
15
|
+
} from './memory-store-core.mjs';
|
|
16
|
+
import { validateMemoryEvent } from './memory-schema.mjs';
|
|
17
|
+
import { readMemoryProjectionSnapshot } from './memory-snapshot-store.mjs';
|
|
18
|
+
import {
|
|
19
|
+
assertVaultPathSafe,
|
|
20
|
+
mkdirVaultPath,
|
|
21
|
+
writeVaultFileAtomic,
|
|
22
|
+
} from './vault-path-safety.mjs';
|
|
23
|
+
|
|
24
|
+
export const MEMORY_SEGMENT_DIRECTORY = 'memory-segments';
|
|
25
|
+
export const MEMORY_SEGMENT_MANIFEST_FILE = 'MEMORY_SEGMENTS.json';
|
|
26
|
+
export const MEMORY_SEGMENT_SCHEMA_VERSION = 1;
|
|
27
|
+
export const MEMORY_SEGMENT_MANIFEST_SCHEMA_VERSION = 1;
|
|
28
|
+
export const MEMORY_SEGMENT_DEFAULT_MAX_EVENTS = 4096;
|
|
29
|
+
export const MEMORY_SEGMENT_DEFAULT_MAX_BYTES = 4 * 1024 * 1024;
|
|
30
|
+
|
|
31
|
+
const SEGMENT_KIND = 'wendkeep-memory-segment';
|
|
32
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
33
|
+
const EVENT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
34
|
+
const SEGMENT_FILE = /^segment-(\d{6})-([a-f0-9]{16})\.jsonl$/;
|
|
35
|
+
const CHAIN_GENESIS = '0'.repeat(64);
|
|
36
|
+
|
|
37
|
+
export class MemorySegmentCorruption extends Error {
|
|
38
|
+
constructor(errors, message = 'Memory segment chain is corrupt; rebuild its manifest before rotation.') {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'MemorySegmentCorruption';
|
|
41
|
+
this.code = 'MEMORY_SEGMENT_CORRUPT';
|
|
42
|
+
this.errors = Array.isArray(errors) ? errors : [String(errors || 'unknown segment error')];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function brainDir(vaultBase) { return join(vaultBase, '.brain'); }
|
|
47
|
+
function segmentRoot(vaultBase) { return join(brainDir(vaultBase), MEMORY_SEGMENT_DIRECTORY); }
|
|
48
|
+
function segmentDataDir(vaultBase) { return join(segmentRoot(vaultBase), 'data'); }
|
|
49
|
+
function manifestPath(vaultBase) { return join(brainDir(vaultBase), MEMORY_SEGMENT_MANIFEST_FILE); }
|
|
50
|
+
function projectPath(vaultBase) { return join(brainDir(vaultBase), 'PROJECT.json'); }
|
|
51
|
+
|
|
52
|
+
function sha256(value) {
|
|
53
|
+
return createHash('sha256').update(value).digest('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function canonicalize(value) {
|
|
57
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
58
|
+
if (value && typeof value === 'object') {
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const key of Object.keys(value).sort()) {
|
|
61
|
+
if (value[key] !== undefined) out[key] = canonicalize(value[key]);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function canonicalJson(value) {
|
|
69
|
+
return JSON.stringify(canonicalize(value));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hashUnsigned(value, excludedKey) {
|
|
73
|
+
const clone = { ...(value || {}) };
|
|
74
|
+
delete clone[excludedKey];
|
|
75
|
+
return sha256(canonicalJson(clone));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function checkedFile(vaultBase, path, label, { allowMissing = true, mustNotExist = false } = {}) {
|
|
79
|
+
return assertVaultPathSafe(vaultBase, path, {
|
|
80
|
+
allowMissing,
|
|
81
|
+
expectedType: 'file',
|
|
82
|
+
mustNotExist,
|
|
83
|
+
label,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function checkedDirectory(vaultBase, path, label, { allowMissing = true } = {}) {
|
|
88
|
+
return assertVaultPathSafe(vaultBase, path, {
|
|
89
|
+
allowMissing,
|
|
90
|
+
expectedType: 'directory',
|
|
91
|
+
label,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readCheckedFile(vaultBase, path, encoding, label, { allowMissing = false } = {}) {
|
|
96
|
+
let checked = checkedFile(vaultBase, path, label, { allowMissing });
|
|
97
|
+
if (!checked.exists) return null;
|
|
98
|
+
checked = checkedFile(vaultBase, checked.target, label, { allowMissing: false });
|
|
99
|
+
return readFileSync(checked.target, encoding);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assertOpenedFile(vaultBase, path, fd, label) {
|
|
103
|
+
const checked = checkedFile(vaultBase, path, label, { allowMissing: false });
|
|
104
|
+
const descriptor = fstatSync(fd, { bigint: true });
|
|
105
|
+
const target = statSync(checked.target, { bigint: true });
|
|
106
|
+
if (!descriptor.isFile() || descriptor.nlink > 1n || target.nlink > 1n
|
|
107
|
+
|| !memoryFileIdentityMatches(descriptor, target)) {
|
|
108
|
+
const error = new Error(`${label} mudou de inode ou possui hardlink antes da escrita.`);
|
|
109
|
+
error.code = 'VAULT_PATH_UNSAFE';
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
return checked.target;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function projectIdForVault(vaultBase) {
|
|
116
|
+
const raw = readCheckedFile(
|
|
117
|
+
vaultBase,
|
|
118
|
+
projectPath(vaultBase),
|
|
119
|
+
'utf8',
|
|
120
|
+
'autoridade PROJECT.json dos segmentos de memória',
|
|
121
|
+
{ allowMissing: true },
|
|
122
|
+
);
|
|
123
|
+
if (raw === null) return '';
|
|
124
|
+
try {
|
|
125
|
+
const project = JSON.parse(raw);
|
|
126
|
+
return typeof project.projectId === 'string' ? project.projectId : '';
|
|
127
|
+
} catch {
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function eventChainStep(previous, event) {
|
|
133
|
+
return sha256(`${previous}\u0000${canonicalMemoryJson(event)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function eventChain(events, initial = CHAIN_GENESIS) {
|
|
137
|
+
return events.reduce((chain, event) => eventChainStep(chain, event), initial);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validPositiveInteger(value, fallback, minimum = 1) {
|
|
141
|
+
const number = Number(value);
|
|
142
|
+
return Number.isInteger(number) && number >= minimum ? number : fallback;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function policyFromOptions(options = {}, fallback = {}) {
|
|
146
|
+
return {
|
|
147
|
+
max_events: validPositiveInteger(
|
|
148
|
+
options.maxEvents ?? options.max_events,
|
|
149
|
+
validPositiveInteger(fallback.max_events, MEMORY_SEGMENT_DEFAULT_MAX_EVENTS),
|
|
150
|
+
),
|
|
151
|
+
max_bytes: validPositiveInteger(
|
|
152
|
+
options.maxBytes ?? options.max_bytes,
|
|
153
|
+
validPositiveInteger(fallback.max_bytes, MEMORY_SEGMENT_DEFAULT_MAX_BYTES),
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function descriptorHash(descriptor) {
|
|
159
|
+
return hashUnsigned(descriptor, 'descriptor_hash');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function manifestHash(manifest) {
|
|
163
|
+
return hashUnsigned(manifest, 'manifest_hash');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function renderManifest(manifest) {
|
|
167
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function segmentFileName(sequence, contentHash) {
|
|
171
|
+
return `segment-${String(sequence).padStart(6, '0')}-${contentHash.slice(0, 16)}.jsonl`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function segmentRelativePath(fileName) {
|
|
175
|
+
return `${MEMORY_SEGMENT_DIRECTORY}/data/${fileName}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildSegment({
|
|
179
|
+
projectId,
|
|
180
|
+
sequence,
|
|
181
|
+
previousDescriptorHash,
|
|
182
|
+
eventChainStart,
|
|
183
|
+
events,
|
|
184
|
+
}) {
|
|
185
|
+
if (!events.length) throw new TypeError('segment must contain at least one event');
|
|
186
|
+
const eventChainEnd = eventChain(events, eventChainStart);
|
|
187
|
+
const header = {
|
|
188
|
+
kind: SEGMENT_KIND,
|
|
189
|
+
schema_version: MEMORY_SEGMENT_SCHEMA_VERSION,
|
|
190
|
+
project_id: projectId,
|
|
191
|
+
sequence,
|
|
192
|
+
previous_descriptor_hash: previousDescriptorHash,
|
|
193
|
+
event_count: events.length,
|
|
194
|
+
first_event_id: events[0].event_id,
|
|
195
|
+
last_event_id: events.at(-1).event_id,
|
|
196
|
+
event_chain_start: eventChainStart,
|
|
197
|
+
event_chain_end: eventChainEnd,
|
|
198
|
+
};
|
|
199
|
+
const content = `${canonicalJson(header)}\n${events.map((event) => canonicalMemoryJson(event)).join('\n')}\n`;
|
|
200
|
+
const contentHash = sha256(content);
|
|
201
|
+
const fileName = segmentFileName(sequence, contentHash);
|
|
202
|
+
const descriptor = {
|
|
203
|
+
sequence,
|
|
204
|
+
file: segmentRelativePath(fileName),
|
|
205
|
+
project_id: projectId,
|
|
206
|
+
event_count: events.length,
|
|
207
|
+
byte_length: Buffer.byteLength(content),
|
|
208
|
+
content_hash: contentHash,
|
|
209
|
+
previous_descriptor_hash: previousDescriptorHash,
|
|
210
|
+
first_event_id: header.first_event_id,
|
|
211
|
+
last_event_id: header.last_event_id,
|
|
212
|
+
event_chain_start: eventChainStart,
|
|
213
|
+
event_chain_end: eventChainEnd,
|
|
214
|
+
};
|
|
215
|
+
descriptor.descriptor_hash = descriptorHash(descriptor);
|
|
216
|
+
return { header, events, content, descriptor, fileName };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildManifest({ projectId, policy, descriptors, snapshot = null }) {
|
|
220
|
+
const segments = descriptors.map((descriptor) => ({ ...descriptor }));
|
|
221
|
+
const last = segments.at(-1);
|
|
222
|
+
const manifest = {
|
|
223
|
+
schema_version: MEMORY_SEGMENT_MANIFEST_SCHEMA_VERSION,
|
|
224
|
+
segment_schema_version: MEMORY_SEGMENT_SCHEMA_VERSION,
|
|
225
|
+
project_id: projectId,
|
|
226
|
+
revision: segments.length,
|
|
227
|
+
policy,
|
|
228
|
+
segment_count: segments.length,
|
|
229
|
+
covered_event_count: segments.reduce((total, segment) => total + segment.event_count, 0),
|
|
230
|
+
covered_bytes: segments.reduce((total, segment) => total + segment.byte_length, 0),
|
|
231
|
+
through_event_id: last?.last_event_id || 'none',
|
|
232
|
+
event_chain_hash: last?.event_chain_end || CHAIN_GENESIS,
|
|
233
|
+
chain_tip: last?.descriptor_hash || CHAIN_GENESIS,
|
|
234
|
+
source_snapshot_hash: snapshot?.snapshot_hash || CHAIN_GENESIS,
|
|
235
|
+
source_snapshot_event_count: Number(snapshot?.event_count || 0),
|
|
236
|
+
segments,
|
|
237
|
+
};
|
|
238
|
+
manifest.manifest_hash = manifestHash(manifest);
|
|
239
|
+
return manifest;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function validateDescriptorShape(descriptor, projectId) {
|
|
243
|
+
return Boolean(descriptor && typeof descriptor === 'object' && !Array.isArray(descriptor))
|
|
244
|
+
&& Number.isInteger(descriptor.sequence) && descriptor.sequence > 0
|
|
245
|
+
&& typeof descriptor.file === 'string'
|
|
246
|
+
&& descriptor.project_id === projectId
|
|
247
|
+
&& Number.isInteger(descriptor.event_count) && descriptor.event_count > 0
|
|
248
|
+
&& Number.isInteger(descriptor.byte_length) && descriptor.byte_length > 0
|
|
249
|
+
&& SHA256.test(String(descriptor.content_hash || ''))
|
|
250
|
+
&& SHA256.test(String(descriptor.previous_descriptor_hash || ''))
|
|
251
|
+
&& EVENT_ID.test(String(descriptor.first_event_id || ''))
|
|
252
|
+
&& EVENT_ID.test(String(descriptor.last_event_id || ''))
|
|
253
|
+
&& SHA256.test(String(descriptor.event_chain_start || ''))
|
|
254
|
+
&& SHA256.test(String(descriptor.event_chain_end || ''))
|
|
255
|
+
&& SHA256.test(String(descriptor.descriptor_hash || ''))
|
|
256
|
+
&& descriptor.descriptor_hash === descriptorHash(descriptor);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function validateManifestShape(manifest, projectId) {
|
|
260
|
+
const errors = [];
|
|
261
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
262
|
+
return ['manifest is not an object'];
|
|
263
|
+
}
|
|
264
|
+
if (manifest.schema_version !== MEMORY_SEGMENT_MANIFEST_SCHEMA_VERSION) errors.push('manifest schema_version mismatch');
|
|
265
|
+
if (manifest.segment_schema_version !== MEMORY_SEGMENT_SCHEMA_VERSION) errors.push('segment schema_version mismatch');
|
|
266
|
+
if (manifest.project_id !== projectId) errors.push('manifest project_id mismatch');
|
|
267
|
+
if (!Array.isArray(manifest.segments)) errors.push('manifest segments missing');
|
|
268
|
+
if (!SHA256.test(String(manifest.source_snapshot_hash || ''))) errors.push('source_snapshot_hash missing');
|
|
269
|
+
if (!Number.isInteger(manifest.source_snapshot_event_count)
|
|
270
|
+
|| manifest.source_snapshot_event_count < manifest.covered_event_count) {
|
|
271
|
+
errors.push('source_snapshot_event_count mismatch');
|
|
272
|
+
}
|
|
273
|
+
if (!manifest.policy || typeof manifest.policy !== 'object') errors.push('manifest policy missing');
|
|
274
|
+
if (!SHA256.test(String(manifest.manifest_hash || '')) || manifest.manifest_hash !== manifestHash(manifest)) {
|
|
275
|
+
errors.push('manifest hash mismatch');
|
|
276
|
+
}
|
|
277
|
+
const segments = Array.isArray(manifest.segments) ? manifest.segments : [];
|
|
278
|
+
if (manifest.revision !== segments.length || manifest.segment_count !== segments.length) {
|
|
279
|
+
errors.push('manifest segment count mismatch');
|
|
280
|
+
}
|
|
281
|
+
let previousDescriptorHash = CHAIN_GENESIS;
|
|
282
|
+
let previousEventChain = CHAIN_GENESIS;
|
|
283
|
+
let events = 0;
|
|
284
|
+
let bytes = 0;
|
|
285
|
+
segments.forEach((descriptor, index) => {
|
|
286
|
+
if (!validateDescriptorShape(descriptor, projectId)) {
|
|
287
|
+
errors.push(`invalid descriptor at index ${index}`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (descriptor.sequence !== index + 1) errors.push(`segment sequence gap at ${descriptor.sequence}`);
|
|
291
|
+
if (descriptor.previous_descriptor_hash !== previousDescriptorHash) {
|
|
292
|
+
errors.push(`descriptor chain mismatch at ${descriptor.sequence}`);
|
|
293
|
+
}
|
|
294
|
+
if (descriptor.event_chain_start !== previousEventChain) {
|
|
295
|
+
errors.push(`event chain mismatch at ${descriptor.sequence}`);
|
|
296
|
+
}
|
|
297
|
+
previousDescriptorHash = descriptor.descriptor_hash;
|
|
298
|
+
previousEventChain = descriptor.event_chain_end;
|
|
299
|
+
events += descriptor.event_count;
|
|
300
|
+
bytes += descriptor.byte_length;
|
|
301
|
+
});
|
|
302
|
+
if (manifest.covered_event_count !== events) errors.push('covered_event_count mismatch');
|
|
303
|
+
if (manifest.covered_bytes !== bytes) errors.push('covered_bytes mismatch');
|
|
304
|
+
if (manifest.chain_tip !== previousDescriptorHash) errors.push('chain_tip mismatch');
|
|
305
|
+
if (manifest.event_chain_hash !== previousEventChain) errors.push('event_chain_hash mismatch');
|
|
306
|
+
if (manifest.through_event_id !== (segments.at(-1)?.last_event_id || 'none')) {
|
|
307
|
+
errors.push('through_event_id mismatch');
|
|
308
|
+
}
|
|
309
|
+
return errors;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function readManifestDocument(vaultBase, projectId) {
|
|
313
|
+
let raw;
|
|
314
|
+
try {
|
|
315
|
+
raw = readCheckedFile(
|
|
316
|
+
vaultBase,
|
|
317
|
+
manifestPath(vaultBase),
|
|
318
|
+
'utf8',
|
|
319
|
+
'manifest dos segmentos de memória',
|
|
320
|
+
{ allowMissing: true },
|
|
321
|
+
);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
324
|
+
return { status: 'invalid', errors: ['manifest unreadable'] };
|
|
325
|
+
}
|
|
326
|
+
if (raw === null) return { status: 'missing', manifest: null, errors: [] };
|
|
327
|
+
let manifest;
|
|
328
|
+
try { manifest = JSON.parse(raw); } catch { return { status: 'invalid', errors: ['manifest invalid JSON'] }; }
|
|
329
|
+
const errors = validateManifestShape(manifest, projectId);
|
|
330
|
+
return errors.length
|
|
331
|
+
? { status: 'invalid', manifest, errors }
|
|
332
|
+
: { status: 'ok', manifest, errors: [] };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function parseSegmentContent(vaultBase, path, descriptor = null, projectId = '') {
|
|
336
|
+
const content = readCheckedFile(vaultBase, path, 'utf8', `segmento de memória ${path}`);
|
|
337
|
+
const errors = [];
|
|
338
|
+
if (!content.endsWith('\n')) errors.push('segment missing final newline');
|
|
339
|
+
const lines = content.split('\n');
|
|
340
|
+
lines.pop();
|
|
341
|
+
if (lines.length < 2) errors.push('segment has no events');
|
|
342
|
+
let header = null;
|
|
343
|
+
try { header = JSON.parse(lines[0] || ''); } catch { errors.push('segment header invalid JSON'); }
|
|
344
|
+
const events = [];
|
|
345
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
346
|
+
const line = lines[index];
|
|
347
|
+
let event;
|
|
348
|
+
try { event = JSON.parse(line); } catch {
|
|
349
|
+
errors.push(`segment event line ${index + 1} invalid JSON`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
|
|
353
|
+
if (!validation.ok) errors.push(`segment event line ${index + 1} invalid: ${validation.errors.join(' ')}`);
|
|
354
|
+
if (canonicalMemoryJson(event) !== line) errors.push(`segment event line ${index + 1} is not canonical`);
|
|
355
|
+
events.push(event);
|
|
356
|
+
}
|
|
357
|
+
const contentHash = sha256(content);
|
|
358
|
+
if (!header || header.kind !== SEGMENT_KIND
|
|
359
|
+
|| header.schema_version !== MEMORY_SEGMENT_SCHEMA_VERSION
|
|
360
|
+
|| header.project_id !== projectId
|
|
361
|
+
|| !Number.isInteger(header.sequence) || header.sequence <= 0
|
|
362
|
+
|| !SHA256.test(String(header.previous_descriptor_hash || ''))
|
|
363
|
+
|| !Number.isInteger(header.event_count) || header.event_count <= 0
|
|
364
|
+
|| !EVENT_ID.test(String(header.first_event_id || ''))
|
|
365
|
+
|| !EVENT_ID.test(String(header.last_event_id || ''))
|
|
366
|
+
|| !SHA256.test(String(header.event_chain_start || ''))
|
|
367
|
+
|| !SHA256.test(String(header.event_chain_end || ''))) {
|
|
368
|
+
errors.push('segment header shape invalid');
|
|
369
|
+
}
|
|
370
|
+
if (header) {
|
|
371
|
+
if (header.event_count !== events.length) errors.push('segment event_count mismatch');
|
|
372
|
+
if (events.length && header.first_event_id !== events[0].event_id) errors.push('segment first_event_id mismatch');
|
|
373
|
+
if (events.length && header.last_event_id !== events.at(-1).event_id) errors.push('segment last_event_id mismatch');
|
|
374
|
+
if (eventChain(events, header.event_chain_start) !== header.event_chain_end) errors.push('segment event chain hash mismatch');
|
|
375
|
+
}
|
|
376
|
+
const sequence = Number(header?.sequence || descriptor?.sequence || 0);
|
|
377
|
+
const fileName = path.split(/[\\/]/).at(-1) || '';
|
|
378
|
+
const expectedName = sequence > 0 ? segmentFileName(sequence, contentHash) : '';
|
|
379
|
+
if (expectedName && fileName !== expectedName) errors.push('segment filename hash mismatch');
|
|
380
|
+
|
|
381
|
+
const computedDescriptor = header && events.length ? {
|
|
382
|
+
sequence: header.sequence,
|
|
383
|
+
file: segmentRelativePath(fileName),
|
|
384
|
+
project_id: header.project_id,
|
|
385
|
+
event_count: header.event_count,
|
|
386
|
+
byte_length: Buffer.byteLength(content),
|
|
387
|
+
content_hash: contentHash,
|
|
388
|
+
previous_descriptor_hash: header.previous_descriptor_hash,
|
|
389
|
+
first_event_id: header.first_event_id,
|
|
390
|
+
last_event_id: header.last_event_id,
|
|
391
|
+
event_chain_start: header.event_chain_start,
|
|
392
|
+
event_chain_end: header.event_chain_end,
|
|
393
|
+
} : null;
|
|
394
|
+
if (computedDescriptor) computedDescriptor.descriptor_hash = descriptorHash(computedDescriptor);
|
|
395
|
+
if (descriptor && computedDescriptor && canonicalJson(descriptor) !== canonicalJson(computedDescriptor)) {
|
|
396
|
+
errors.push('segment descriptor does not match file content');
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
status: errors.length ? 'invalid' : 'ok',
|
|
400
|
+
errors,
|
|
401
|
+
content,
|
|
402
|
+
header,
|
|
403
|
+
events,
|
|
404
|
+
descriptor: computedDescriptor,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function segmentPathFromDescriptor(vaultBase, descriptor) {
|
|
409
|
+
const expectedPrefix = `${MEMORY_SEGMENT_DIRECTORY}/data/`;
|
|
410
|
+
if (!descriptor.file.startsWith(expectedPrefix) || descriptor.file.includes('..') || descriptor.file.includes('\\')) {
|
|
411
|
+
throw new MemorySegmentCorruption([`unsafe segment path: ${descriptor.file}`]);
|
|
412
|
+
}
|
|
413
|
+
return join(brainDir(vaultBase), ...descriptor.file.split('/'));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function verifyManifestFiles(vaultBase, manifest) {
|
|
417
|
+
const errors = [];
|
|
418
|
+
const events = [];
|
|
419
|
+
for (const descriptor of manifest.segments) {
|
|
420
|
+
let parsed;
|
|
421
|
+
try {
|
|
422
|
+
parsed = parseSegmentContent(
|
|
423
|
+
vaultBase,
|
|
424
|
+
segmentPathFromDescriptor(vaultBase, descriptor),
|
|
425
|
+
descriptor,
|
|
426
|
+
manifest.project_id,
|
|
427
|
+
);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
430
|
+
errors.push(`segment ${descriptor.sequence} unreadable`);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
errors.push(...parsed.errors.map((error) => `segment ${descriptor.sequence}: ${error}`));
|
|
434
|
+
events.push(...parsed.events);
|
|
435
|
+
}
|
|
436
|
+
return { errors, events };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function compareEventsWithLedger(segmentEvents, ledgerEvents) {
|
|
440
|
+
const errors = [];
|
|
441
|
+
if (segmentEvents.length > ledgerEvents.length) return ['segment chain is longer than ledger authority'];
|
|
442
|
+
for (let index = 0; index < segmentEvents.length; index += 1) {
|
|
443
|
+
if (canonicalMemoryJson(segmentEvents[index]) !== canonicalMemoryJson(ledgerEvents[index])) {
|
|
444
|
+
errors.push(`ledger prefix diverges at event ${index + 1}`);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return errors;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function verifyLocked(vaultBase, { verifyLedger = true, verifySnapshot = true } = {}) {
|
|
452
|
+
const projectId = projectIdForVault(vaultBase);
|
|
453
|
+
const loaded = readManifestDocument(vaultBase, projectId);
|
|
454
|
+
if (loaded.status === 'missing') {
|
|
455
|
+
return {
|
|
456
|
+
status: 'missing',
|
|
457
|
+
valid: true,
|
|
458
|
+
projectId,
|
|
459
|
+
segments: 0,
|
|
460
|
+
coveredEvents: 0,
|
|
461
|
+
errors: [],
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (loaded.status !== 'ok') {
|
|
465
|
+
return {
|
|
466
|
+
status: 'invalid',
|
|
467
|
+
valid: false,
|
|
468
|
+
projectId,
|
|
469
|
+
segments: loaded.manifest?.segments?.length || 0,
|
|
470
|
+
coveredEvents: loaded.manifest?.covered_event_count || 0,
|
|
471
|
+
errors: loaded.errors,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const files = verifyManifestFiles(vaultBase, loaded.manifest);
|
|
475
|
+
const errors = [...files.errors];
|
|
476
|
+
let ledger = null;
|
|
477
|
+
if (verifyLedger) {
|
|
478
|
+
ledger = readMemoryLedger(vaultBase);
|
|
479
|
+
if (ledger.status !== 'ok') {
|
|
480
|
+
errors.push(...ledger.errors.map((item) => `ledger line ${item.line}: ${item.message}`));
|
|
481
|
+
} else {
|
|
482
|
+
errors.push(...compareEventsWithLedger(files.events, ledger.events));
|
|
483
|
+
if (eventChain(files.events) !== loaded.manifest.event_chain_hash) {
|
|
484
|
+
errors.push('manifest event chain does not match segment events');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
let snapshot = null;
|
|
489
|
+
if (verifySnapshot) {
|
|
490
|
+
snapshot = readMemoryProjectionSnapshot(vaultBase);
|
|
491
|
+
if (snapshot.status !== 'ok' || snapshot.tail?.status !== 'ok') {
|
|
492
|
+
errors.push(`snapshot unavailable for segment verification: ${snapshot.reason || snapshot.tail?.reason || snapshot.status}`);
|
|
493
|
+
} else if (snapshot.snapshot.event_count < loaded.manifest.covered_event_count) {
|
|
494
|
+
errors.push('snapshot boundary is older than sealed segment chain');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
status: errors.length ? 'invalid' : 'ok',
|
|
499
|
+
valid: errors.length === 0,
|
|
500
|
+
projectId,
|
|
501
|
+
manifest: loaded.manifest,
|
|
502
|
+
segments: loaded.manifest.segment_count,
|
|
503
|
+
coveredEvents: loaded.manifest.covered_event_count,
|
|
504
|
+
coveredBytes: loaded.manifest.covered_bytes,
|
|
505
|
+
chainTip: loaded.manifest.chain_tip,
|
|
506
|
+
manifestHash: loaded.manifest.manifest_hash,
|
|
507
|
+
errors,
|
|
508
|
+
_events: files.events,
|
|
509
|
+
_ledger: ledger,
|
|
510
|
+
_snapshot: snapshot,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function readMemorySegmentManifest(vaultBase) {
|
|
515
|
+
const projectId = projectIdForVault(vaultBase);
|
|
516
|
+
const loaded = readManifestDocument(vaultBase, projectId);
|
|
517
|
+
return loaded.status === 'ok'
|
|
518
|
+
? { status: 'ok', manifest: loaded.manifest }
|
|
519
|
+
: { status: loaded.status, errors: loaded.errors };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function verifyMemorySegments(vaultBase, options = {}) {
|
|
523
|
+
const result = verifyLocked(vaultBase, options);
|
|
524
|
+
const { _events, _ledger, _snapshot, ...publicResult } = result;
|
|
525
|
+
return publicResult;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function writeImmutableSegment(vaultBase, segment) {
|
|
529
|
+
const dir = segmentDataDir(vaultBase);
|
|
530
|
+
mkdirVaultPath(vaultBase, dir, { label: 'diretório de dados dos segmentos de memória' });
|
|
531
|
+
const path = join(dir, segment.fileName);
|
|
532
|
+
const checked = checkedFile(vaultBase, path, `segmento imutável ${segment.fileName}`);
|
|
533
|
+
if (checked.exists) {
|
|
534
|
+
const existing = readCheckedFile(vaultBase, checked.target, 'utf8', `segmento imutável ${segment.fileName}`);
|
|
535
|
+
if (existing !== segment.content) {
|
|
536
|
+
throw new MemorySegmentCorruption([`immutable segment content diverged: ${segment.fileName}`]);
|
|
537
|
+
}
|
|
538
|
+
return { status: 'existing', path };
|
|
539
|
+
}
|
|
540
|
+
writeVaultFileAtomic(vaultBase, path, segment.content, 'utf8', {
|
|
541
|
+
label: `segmento imutável ${segment.fileName}`,
|
|
542
|
+
scopeRoot: dir,
|
|
543
|
+
beforeRename: () => checkedFile(
|
|
544
|
+
vaultBase,
|
|
545
|
+
path,
|
|
546
|
+
`destino imutável ${segment.fileName}`,
|
|
547
|
+
{ mustNotExist: true },
|
|
548
|
+
),
|
|
549
|
+
});
|
|
550
|
+
return { status: 'written', path };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function partitionEvents(events, policy, force) {
|
|
554
|
+
const chunks = [];
|
|
555
|
+
let current = [];
|
|
556
|
+
let currentBytes = 0;
|
|
557
|
+
const flush = () => {
|
|
558
|
+
if (!current.length) return;
|
|
559
|
+
chunks.push(current);
|
|
560
|
+
current = [];
|
|
561
|
+
currentBytes = 0;
|
|
562
|
+
};
|
|
563
|
+
for (const event of events) {
|
|
564
|
+
const bytes = Buffer.byteLength(`${canonicalMemoryJson(event)}\n`);
|
|
565
|
+
if (current.length && (current.length >= policy.max_events || currentBytes + bytes > policy.max_bytes)) {
|
|
566
|
+
flush();
|
|
567
|
+
}
|
|
568
|
+
current.push(event);
|
|
569
|
+
currentBytes += bytes;
|
|
570
|
+
if (current.length >= policy.max_events || currentBytes >= policy.max_bytes) flush();
|
|
571
|
+
}
|
|
572
|
+
if (current.length && force) flush();
|
|
573
|
+
return {
|
|
574
|
+
chunks,
|
|
575
|
+
pending: current.length,
|
|
576
|
+
pendingBytes: currentBytes,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function writeManifestIfChanged(vaultBase, manifest) {
|
|
581
|
+
const path = manifestPath(vaultBase);
|
|
582
|
+
const content = renderManifest(manifest);
|
|
583
|
+
const current = readCheckedFile(
|
|
584
|
+
vaultBase,
|
|
585
|
+
path,
|
|
586
|
+
'utf8',
|
|
587
|
+
'manifest dos segmentos de memória',
|
|
588
|
+
{ allowMissing: true },
|
|
589
|
+
);
|
|
590
|
+
if (current === content) return { status: 'unchanged', path };
|
|
591
|
+
writeVaultFileAtomic(vaultBase, path, content, 'utf8', {
|
|
592
|
+
label: 'manifest dos segmentos de memória',
|
|
593
|
+
scopeRoot: brainDir(vaultBase),
|
|
594
|
+
});
|
|
595
|
+
return { status: 'written', path };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function injectFault(faultAt, boundary) {
|
|
599
|
+
if (faultAt === boundary) throw new Error(`Injected memory-segment fault: ${boundary}`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function sealLocked(vaultBase, options = {}) {
|
|
603
|
+
mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain dos segmentos de memória' });
|
|
604
|
+
mkdirVaultPath(vaultBase, segmentRoot(vaultBase), { label: 'raiz dos segmentos de memória' });
|
|
605
|
+
mkdirVaultPath(vaultBase, segmentDataDir(vaultBase), { label: 'diretório de dados dos segmentos de memória' });
|
|
606
|
+
|
|
607
|
+
const projectId = projectIdForVault(vaultBase);
|
|
608
|
+
const snapshot = readMemoryProjectionSnapshot(vaultBase);
|
|
609
|
+
if (snapshot.status !== 'ok' || snapshot.tail?.status !== 'ok') {
|
|
610
|
+
throw new MemorySegmentCorruption([
|
|
611
|
+
`valid snapshot required: ${snapshot.reason || snapshot.tail?.reason || snapshot.status}`,
|
|
612
|
+
]);
|
|
613
|
+
}
|
|
614
|
+
const ledger = readMemoryLedger(vaultBase);
|
|
615
|
+
if (ledger.status !== 'ok') throw new MemoryLedgerCorruption(ledger.errors);
|
|
616
|
+
const boundaryCount = snapshot.snapshot.event_count;
|
|
617
|
+
if (boundaryCount > ledger.events.length) {
|
|
618
|
+
throw new MemorySegmentCorruption(['snapshot event_count exceeds ledger authority']);
|
|
619
|
+
}
|
|
620
|
+
const snapshotEvents = ledger.events.slice(0, boundaryCount);
|
|
621
|
+
if (boundaryCount > 0 && snapshotEvents.at(-1)?.event_id !== snapshot.snapshot.through_event_id) {
|
|
622
|
+
throw new MemorySegmentCorruption(['snapshot through_event_id diverges from ledger authority']);
|
|
623
|
+
}
|
|
624
|
+
if (eventChain(snapshotEvents) !== snapshot.snapshot.chain_hash) {
|
|
625
|
+
throw new MemorySegmentCorruption(['snapshot chain_hash diverges from ledger authority']);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const current = verifyLocked(vaultBase, { verifyLedger: true, verifySnapshot: false });
|
|
629
|
+
if (current.status === 'invalid') throw new MemorySegmentCorruption(current.errors);
|
|
630
|
+
const existing = current.manifest || buildManifest({
|
|
631
|
+
projectId,
|
|
632
|
+
policy: policyFromOptions(options),
|
|
633
|
+
descriptors: [],
|
|
634
|
+
snapshot: snapshot.snapshot,
|
|
635
|
+
});
|
|
636
|
+
if (existing.covered_event_count > boundaryCount) {
|
|
637
|
+
throw new MemorySegmentCorruption(['segment chain extends beyond current snapshot boundary']);
|
|
638
|
+
}
|
|
639
|
+
const policy = policyFromOptions(options, existing.policy);
|
|
640
|
+
const remaining = snapshotEvents.slice(existing.covered_event_count);
|
|
641
|
+
const partitioned = partitionEvents(remaining, policy, options.force === true);
|
|
642
|
+
if (!partitioned.chunks.length) {
|
|
643
|
+
return {
|
|
644
|
+
status: 'noop',
|
|
645
|
+
segments: existing.segment_count,
|
|
646
|
+
createdSegments: 0,
|
|
647
|
+
coveredEvents: existing.covered_event_count,
|
|
648
|
+
pendingEvents: remaining.length,
|
|
649
|
+
pendingBytes: partitioned.pendingBytes,
|
|
650
|
+
manifestHash: existing.manifest_hash,
|
|
651
|
+
chainTip: existing.chain_tip,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const descriptors = existing.segments.map((descriptor) => ({ ...descriptor }));
|
|
656
|
+
let previousDescriptorHash = existing.chain_tip;
|
|
657
|
+
let chain = existing.event_chain_hash;
|
|
658
|
+
let written = 0;
|
|
659
|
+
let reused = 0;
|
|
660
|
+
for (const events of partitioned.chunks) {
|
|
661
|
+
const sequence = descriptors.length + 1;
|
|
662
|
+
const segment = buildSegment({
|
|
663
|
+
projectId,
|
|
664
|
+
sequence,
|
|
665
|
+
previousDescriptorHash,
|
|
666
|
+
eventChainStart: chain,
|
|
667
|
+
events,
|
|
668
|
+
});
|
|
669
|
+
const result = writeImmutableSegment(vaultBase, segment);
|
|
670
|
+
if (result.status === 'written') written += 1;
|
|
671
|
+
else reused += 1;
|
|
672
|
+
descriptors.push(segment.descriptor);
|
|
673
|
+
previousDescriptorHash = segment.descriptor.descriptor_hash;
|
|
674
|
+
chain = segment.descriptor.event_chain_end;
|
|
675
|
+
injectFault(options.faultAt, 'after-segment');
|
|
676
|
+
injectFault(options.faultAt, `after-segment-${sequence}`);
|
|
677
|
+
}
|
|
678
|
+
const manifest = buildManifest({ projectId, policy, descriptors, snapshot: snapshot.snapshot });
|
|
679
|
+
const manifestResult = writeManifestIfChanged(vaultBase, manifest);
|
|
680
|
+
injectFault(options.faultAt, 'after-manifest');
|
|
681
|
+
return {
|
|
682
|
+
status: 'sealed',
|
|
683
|
+
segments: manifest.segment_count,
|
|
684
|
+
createdSegments: written,
|
|
685
|
+
reusedSegments: reused,
|
|
686
|
+
coveredEvents: manifest.covered_event_count,
|
|
687
|
+
pendingEvents: boundaryCount - manifest.covered_event_count,
|
|
688
|
+
pendingBytes: partitioned.pendingBytes,
|
|
689
|
+
manifestStatus: manifestResult.status,
|
|
690
|
+
manifestHash: manifest.manifest_hash,
|
|
691
|
+
chainTip: manifest.chain_tip,
|
|
692
|
+
eventChainHash: manifest.event_chain_hash,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function sealMemorySegments(vaultBase, options = {}) {
|
|
697
|
+
const result = withMemoryLock(
|
|
698
|
+
vaultBase,
|
|
699
|
+
() => sealLocked(vaultBase, options),
|
|
700
|
+
options.lock || {},
|
|
701
|
+
);
|
|
702
|
+
if (result === MEMORY_LOCK_BUSY) return { status: 'busy' };
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function scanSegmentFiles(vaultBase, projectId) {
|
|
707
|
+
const dir = segmentDataDir(vaultBase);
|
|
708
|
+
let checked = checkedDirectory(vaultBase, dir, 'diretório de dados dos segmentos de memória');
|
|
709
|
+
if (!checked.exists) return { descriptors: [], events: [], errors: [], unknown: [] };
|
|
710
|
+
checked = checkedDirectory(vaultBase, checked.target, 'diretório de dados dos segmentos de memória', {
|
|
711
|
+
allowMissing: false,
|
|
712
|
+
});
|
|
713
|
+
const entries = readdirSync(checked.target, { withFileTypes: true });
|
|
714
|
+
const errors = [];
|
|
715
|
+
const unknown = [];
|
|
716
|
+
const parsed = [];
|
|
717
|
+
for (const entry of entries) {
|
|
718
|
+
const path = join(checked.target, entry.name);
|
|
719
|
+
assertVaultPathSafe(vaultBase, path, {
|
|
720
|
+
allowMissing: false,
|
|
721
|
+
label: `entrada ${entry.name} dos segmentos de memória`,
|
|
722
|
+
});
|
|
723
|
+
if (!entry.isFile() || !SEGMENT_FILE.test(entry.name)) {
|
|
724
|
+
unknown.push(entry.name);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
const result = parseSegmentContent(vaultBase, path, null, projectId);
|
|
728
|
+
if (result.status !== 'ok') {
|
|
729
|
+
errors.push(...result.errors.map((error) => `${entry.name}: ${error}`));
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
parsed.push(result);
|
|
733
|
+
}
|
|
734
|
+
parsed.sort((left, right) => left.descriptor.sequence - right.descriptor.sequence);
|
|
735
|
+
const descriptors = [];
|
|
736
|
+
const events = [];
|
|
737
|
+
let previousDescriptorHash = CHAIN_GENESIS;
|
|
738
|
+
let previousEventChain = CHAIN_GENESIS;
|
|
739
|
+
for (let index = 0; index < parsed.length; index += 1) {
|
|
740
|
+
const item = parsed[index];
|
|
741
|
+
const descriptor = item.descriptor;
|
|
742
|
+
if (descriptor.sequence !== index + 1) errors.push(`segment sequence gap at ${descriptor.sequence}`);
|
|
743
|
+
if (descriptors.some((entry) => entry.sequence === descriptor.sequence)) {
|
|
744
|
+
errors.push(`duplicate segment sequence ${descriptor.sequence}`);
|
|
745
|
+
}
|
|
746
|
+
if (descriptor.previous_descriptor_hash !== previousDescriptorHash) {
|
|
747
|
+
errors.push(`descriptor chain mismatch at ${descriptor.sequence}`);
|
|
748
|
+
}
|
|
749
|
+
if (descriptor.event_chain_start !== previousEventChain) {
|
|
750
|
+
errors.push(`event chain mismatch at ${descriptor.sequence}`);
|
|
751
|
+
}
|
|
752
|
+
descriptors.push(descriptor);
|
|
753
|
+
events.push(...item.events);
|
|
754
|
+
previousDescriptorHash = descriptor.descriptor_hash;
|
|
755
|
+
previousEventChain = descriptor.event_chain_end;
|
|
756
|
+
}
|
|
757
|
+
return { descriptors, events, errors, unknown };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function repairPlan(vaultBase, options = {}) {
|
|
761
|
+
const projectId = projectIdForVault(vaultBase);
|
|
762
|
+
const scanned = scanSegmentFiles(vaultBase, projectId);
|
|
763
|
+
const errors = [...scanned.errors];
|
|
764
|
+
const ledger = readMemoryLedger(vaultBase);
|
|
765
|
+
if (ledger.status !== 'ok') {
|
|
766
|
+
errors.push(...ledger.errors.map((item) => `ledger line ${item.line}: ${item.message}`));
|
|
767
|
+
} else {
|
|
768
|
+
errors.push(...compareEventsWithLedger(scanned.events, ledger.events));
|
|
769
|
+
}
|
|
770
|
+
const snapshot = readMemoryProjectionSnapshot(vaultBase);
|
|
771
|
+
if (snapshot.status !== 'ok' || snapshot.tail?.status !== 'ok') {
|
|
772
|
+
errors.push(`snapshot unavailable: ${snapshot.reason || snapshot.tail?.reason || snapshot.status}`);
|
|
773
|
+
} else if (snapshot.snapshot.event_count < scanned.events.length) {
|
|
774
|
+
errors.push('snapshot boundary is older than scanned segment chain');
|
|
775
|
+
}
|
|
776
|
+
const current = readManifestDocument(vaultBase, projectId);
|
|
777
|
+
const policy = policyFromOptions(options, current.manifest?.policy || {});
|
|
778
|
+
const manifest = buildManifest({
|
|
779
|
+
projectId,
|
|
780
|
+
policy,
|
|
781
|
+
descriptors: scanned.descriptors,
|
|
782
|
+
snapshot: snapshot.status === 'ok' ? snapshot.snapshot : null,
|
|
783
|
+
});
|
|
784
|
+
const currentHash = current.status === 'ok' ? current.manifest.manifest_hash : null;
|
|
785
|
+
return {
|
|
786
|
+
ok: errors.length === 0,
|
|
787
|
+
errors,
|
|
788
|
+
unknown: scanned.unknown,
|
|
789
|
+
manifest,
|
|
790
|
+
currentStatus: current.status,
|
|
791
|
+
currentHash,
|
|
792
|
+
changed: currentHash !== manifest.manifest_hash,
|
|
793
|
+
segments: scanned.descriptors.length,
|
|
794
|
+
coveredEvents: scanned.events.length,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export function repairMemorySegmentManifest(vaultBase, options = {}) {
|
|
799
|
+
const preview = repairPlan(vaultBase, options);
|
|
800
|
+
if (!options.apply || !preview.ok) {
|
|
801
|
+
return {
|
|
802
|
+
status: preview.ok ? 'preview' : 'blocked',
|
|
803
|
+
apply: false,
|
|
804
|
+
...preview,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const result = withMemoryLock(vaultBase, () => {
|
|
808
|
+
const fresh = repairPlan(vaultBase, options);
|
|
809
|
+
if (!fresh.ok) return { status: 'blocked', apply: false, ...fresh };
|
|
810
|
+
const publication = writeManifestIfChanged(vaultBase, fresh.manifest);
|
|
811
|
+
return {
|
|
812
|
+
status: publication.status === 'unchanged' ? 'unchanged' : 'repaired',
|
|
813
|
+
apply: true,
|
|
814
|
+
publication: publication.status,
|
|
815
|
+
...fresh,
|
|
816
|
+
};
|
|
817
|
+
}, options.lock || {});
|
|
818
|
+
if (result === MEMORY_LOCK_BUSY) return { status: 'busy', apply: false };
|
|
819
|
+
return result;
|
|
820
|
+
}
|