@soulcraft/brainy 5.2.1 → 5.3.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 +5 -0
- package/dist/augmentations/versioningAugmentation.d.ts +121 -0
- package/dist/augmentations/versioningAugmentation.js +418 -0
- package/dist/brainy.d.ts +28 -0
- package/dist/brainy.js +33 -1
- package/dist/versioning/VersionDiff.d.ts +112 -0
- package/dist/versioning/VersionDiff.js +320 -0
- package/dist/versioning/VersionIndex.d.ts +126 -0
- package/dist/versioning/VersionIndex.js +275 -0
- package/dist/versioning/VersionManager.d.ts +195 -0
- package/dist/versioning/VersionManager.js +352 -0
- package/dist/versioning/VersionStorage.d.ts +101 -0
- package/dist/versioning/VersionStorage.js +222 -0
- package/dist/versioning/VersioningAPI.d.ts +347 -0
- package/dist/versioning/VersioningAPI.js +432 -0
- package/dist/vfs/VirtualFileSystem.js +8 -4
- package/dist/vfs/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VersionManager - Entity-Level Versioning Engine (v5.3.0)
|
|
3
|
+
*
|
|
4
|
+
* Provides entity-level version control with:
|
|
5
|
+
* - save() - Create entity version
|
|
6
|
+
* - restore() - Restore entity to specific version
|
|
7
|
+
* - list() - List all versions of an entity
|
|
8
|
+
* - compare() - Deep diff between versions
|
|
9
|
+
* - prune() - Remove old versions (retention policies)
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* - Hybrid storage: COW commits for full snapshots + version index for fast queries
|
|
13
|
+
* - Content-addressable: SHA-256 hashing for deduplication
|
|
14
|
+
* - Space-efficient: Only stores changed data
|
|
15
|
+
* - Branch-aware: Versions tied to current branch
|
|
16
|
+
*
|
|
17
|
+
* NO MOCKS - Production implementation
|
|
18
|
+
*/
|
|
19
|
+
import { VersionStorage } from './VersionStorage.js';
|
|
20
|
+
import { VersionIndex } from './VersionIndex.js';
|
|
21
|
+
import { compareEntityVersions } from './VersionDiff.js';
|
|
22
|
+
/**
|
|
23
|
+
* VersionManager - Core versioning engine
|
|
24
|
+
*/
|
|
25
|
+
export class VersionManager {
|
|
26
|
+
constructor(brain) {
|
|
27
|
+
this.initialized = false;
|
|
28
|
+
this.brain = brain;
|
|
29
|
+
this.versionStorage = new VersionStorage(brain);
|
|
30
|
+
this.versionIndex = new VersionIndex(brain);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Initialize versioning system (lazy)
|
|
34
|
+
*/
|
|
35
|
+
async initialize() {
|
|
36
|
+
if (this.initialized)
|
|
37
|
+
return;
|
|
38
|
+
await this.versionStorage.initialize();
|
|
39
|
+
await this.versionIndex.initialize();
|
|
40
|
+
this.initialized = true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Save a version of an entity
|
|
44
|
+
*
|
|
45
|
+
* Creates a version snapshot of the current entity state.
|
|
46
|
+
* If createCommit is true, also creates a commit containing this version.
|
|
47
|
+
*
|
|
48
|
+
* @param entityId Entity ID to version
|
|
49
|
+
* @param options Save options
|
|
50
|
+
* @returns Created version metadata
|
|
51
|
+
*/
|
|
52
|
+
async save(entityId, options = {}) {
|
|
53
|
+
await this.initialize();
|
|
54
|
+
// Get current entity state
|
|
55
|
+
const entity = await this.brain.getNounMetadata(entityId);
|
|
56
|
+
if (!entity) {
|
|
57
|
+
throw new Error(`Entity ${entityId} not found`);
|
|
58
|
+
}
|
|
59
|
+
// Get current branch
|
|
60
|
+
const currentBranch = this.brain.currentBranch;
|
|
61
|
+
// Get next version number
|
|
62
|
+
const existingVersions = await this.versionIndex.getVersions({
|
|
63
|
+
entityId,
|
|
64
|
+
branch: currentBranch
|
|
65
|
+
});
|
|
66
|
+
const nextVersion = existingVersions.length + 1;
|
|
67
|
+
// Calculate content hash
|
|
68
|
+
const contentHash = this.versionStorage.hashEntity(entity);
|
|
69
|
+
// Check for duplicate (same content as last version)
|
|
70
|
+
if (existingVersions.length > 0) {
|
|
71
|
+
const lastVersion = existingVersions[existingVersions.length - 1];
|
|
72
|
+
if (lastVersion.contentHash === contentHash) {
|
|
73
|
+
// Content unchanged - return last version instead of creating duplicate
|
|
74
|
+
return lastVersion;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Create commit if requested
|
|
78
|
+
let commitHash;
|
|
79
|
+
if (options.createCommit) {
|
|
80
|
+
const commitMessage = options.commitMessage || `Version ${nextVersion} of entity ${entityId}`;
|
|
81
|
+
// Use brain's commit method (note: single options object)
|
|
82
|
+
await this.brain.commit({
|
|
83
|
+
message: commitMessage,
|
|
84
|
+
author: options.author,
|
|
85
|
+
metadata: {
|
|
86
|
+
versionedEntity: entityId,
|
|
87
|
+
version: nextVersion,
|
|
88
|
+
...options.metadata
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// Get the commit hash that was just created
|
|
92
|
+
const refManager = this.brain.refManager;
|
|
93
|
+
const ref = await refManager.getRef(currentBranch);
|
|
94
|
+
commitHash = ref;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Use current HEAD commit
|
|
98
|
+
const refManager = this.brain.refManager;
|
|
99
|
+
const ref = await refManager.getRef(currentBranch);
|
|
100
|
+
if (!ref) {
|
|
101
|
+
throw new Error(`No commit exists on branch ${currentBranch}. Create a commit first or use createCommit: true`);
|
|
102
|
+
}
|
|
103
|
+
commitHash = ref;
|
|
104
|
+
}
|
|
105
|
+
// Create version metadata
|
|
106
|
+
const version = {
|
|
107
|
+
version: nextVersion,
|
|
108
|
+
entityId,
|
|
109
|
+
branch: currentBranch,
|
|
110
|
+
commitHash,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
contentHash,
|
|
113
|
+
tag: options.tag,
|
|
114
|
+
description: options.description,
|
|
115
|
+
author: options.author,
|
|
116
|
+
metadata: options.metadata
|
|
117
|
+
};
|
|
118
|
+
// Store version
|
|
119
|
+
await this.versionStorage.saveVersion(version, entity);
|
|
120
|
+
await this.versionIndex.addVersion(version);
|
|
121
|
+
return version;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get all versions of an entity
|
|
125
|
+
*
|
|
126
|
+
* @param entityId Entity ID
|
|
127
|
+
* @param query Optional query filters
|
|
128
|
+
* @returns List of versions (newest first)
|
|
129
|
+
*/
|
|
130
|
+
async list(entityId, query = {}) {
|
|
131
|
+
await this.initialize();
|
|
132
|
+
const currentBranch = this.brain.currentBranch;
|
|
133
|
+
return this.versionIndex.getVersions({
|
|
134
|
+
entityId,
|
|
135
|
+
branch: query.branch || currentBranch,
|
|
136
|
+
limit: query.limit,
|
|
137
|
+
offset: query.offset,
|
|
138
|
+
tag: query.tag,
|
|
139
|
+
startDate: query.startDate,
|
|
140
|
+
endDate: query.endDate
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get a specific version of an entity
|
|
145
|
+
*
|
|
146
|
+
* @param entityId Entity ID
|
|
147
|
+
* @param version Version number (1-indexed)
|
|
148
|
+
* @returns Version metadata
|
|
149
|
+
*/
|
|
150
|
+
async getVersion(entityId, version) {
|
|
151
|
+
await this.initialize();
|
|
152
|
+
const currentBranch = this.brain.currentBranch;
|
|
153
|
+
return this.versionIndex.getVersion(entityId, version, currentBranch);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get version by tag
|
|
157
|
+
*
|
|
158
|
+
* @param entityId Entity ID
|
|
159
|
+
* @param tag Version tag
|
|
160
|
+
* @returns Version metadata
|
|
161
|
+
*/
|
|
162
|
+
async getVersionByTag(entityId, tag) {
|
|
163
|
+
await this.initialize();
|
|
164
|
+
const currentBranch = this.brain.currentBranch;
|
|
165
|
+
return this.versionIndex.getVersionByTag(entityId, tag, currentBranch);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Restore entity to a specific version
|
|
169
|
+
*
|
|
170
|
+
* Overwrites current entity state with the specified version.
|
|
171
|
+
* Optionally creates a snapshot before restoring for undo capability.
|
|
172
|
+
*
|
|
173
|
+
* @param entityId Entity ID
|
|
174
|
+
* @param version Version number or tag
|
|
175
|
+
* @param options Restore options
|
|
176
|
+
* @returns Restored version metadata
|
|
177
|
+
*/
|
|
178
|
+
async restore(entityId, version, options = {}) {
|
|
179
|
+
await this.initialize();
|
|
180
|
+
// Create snapshot before restoring (for undo)
|
|
181
|
+
if (options.createSnapshot) {
|
|
182
|
+
await this.save(entityId, {
|
|
183
|
+
tag: options.snapshotTag || 'before-restore',
|
|
184
|
+
description: `Snapshot before restoring to version ${version}`,
|
|
185
|
+
metadata: { restoringTo: version }
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Get target version
|
|
189
|
+
let targetVersion;
|
|
190
|
+
if (typeof version === 'number') {
|
|
191
|
+
targetVersion = await this.getVersion(entityId, version);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
targetVersion = await this.getVersionByTag(entityId, version);
|
|
195
|
+
}
|
|
196
|
+
if (!targetVersion) {
|
|
197
|
+
throw new Error(`Version ${version} not found for entity ${entityId}`);
|
|
198
|
+
}
|
|
199
|
+
// Load versioned entity data
|
|
200
|
+
const versionedEntity = await this.versionStorage.loadVersion(targetVersion);
|
|
201
|
+
if (!versionedEntity) {
|
|
202
|
+
throw new Error(`Version data not found for entity ${entityId} version ${version}`);
|
|
203
|
+
}
|
|
204
|
+
// Restore entity in storage
|
|
205
|
+
await this.brain.saveNounMetadata(entityId, versionedEntity);
|
|
206
|
+
return targetVersion;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Compare two versions of an entity
|
|
210
|
+
*
|
|
211
|
+
* @param entityId Entity ID
|
|
212
|
+
* @param fromVersion Version number or tag (older)
|
|
213
|
+
* @param toVersion Version number or tag (newer)
|
|
214
|
+
* @returns Diff between versions
|
|
215
|
+
*/
|
|
216
|
+
async compare(entityId, fromVersion, toVersion) {
|
|
217
|
+
await this.initialize();
|
|
218
|
+
// Get versions
|
|
219
|
+
const fromVer = typeof fromVersion === 'number'
|
|
220
|
+
? await this.getVersion(entityId, fromVersion)
|
|
221
|
+
: await this.getVersionByTag(entityId, fromVersion);
|
|
222
|
+
const toVer = typeof toVersion === 'number'
|
|
223
|
+
? await this.getVersion(entityId, toVersion)
|
|
224
|
+
: await this.getVersionByTag(entityId, toVersion);
|
|
225
|
+
if (!fromVer) {
|
|
226
|
+
throw new Error(`Version ${fromVersion} not found for entity ${entityId}`);
|
|
227
|
+
}
|
|
228
|
+
if (!toVer) {
|
|
229
|
+
throw new Error(`Version ${toVersion} not found for entity ${entityId}`);
|
|
230
|
+
}
|
|
231
|
+
// Load entity data
|
|
232
|
+
const fromEntity = await this.versionStorage.loadVersion(fromVer);
|
|
233
|
+
const toEntity = await this.versionStorage.loadVersion(toVer);
|
|
234
|
+
if (!fromEntity || !toEntity) {
|
|
235
|
+
throw new Error('Failed to load version data for comparison');
|
|
236
|
+
}
|
|
237
|
+
// Compare versions
|
|
238
|
+
return compareEntityVersions(fromEntity, toEntity, {
|
|
239
|
+
fromVersion: fromVer.version,
|
|
240
|
+
toVersion: toVer.version,
|
|
241
|
+
entityId
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Prune old versions based on retention policy
|
|
246
|
+
*
|
|
247
|
+
* @param entityId Entity ID (or '*' for all entities)
|
|
248
|
+
* @param options Prune options
|
|
249
|
+
* @returns Number of versions deleted
|
|
250
|
+
*/
|
|
251
|
+
async prune(entityId, options) {
|
|
252
|
+
await this.initialize();
|
|
253
|
+
if (!options.keepRecent && !options.keepAfter) {
|
|
254
|
+
throw new Error('Must specify either keepRecent or keepAfter in prune options');
|
|
255
|
+
}
|
|
256
|
+
const currentBranch = this.brain.currentBranch;
|
|
257
|
+
// Get all versions
|
|
258
|
+
const versions = await this.versionIndex.getVersions({
|
|
259
|
+
entityId,
|
|
260
|
+
branch: currentBranch
|
|
261
|
+
});
|
|
262
|
+
// Determine which versions to keep
|
|
263
|
+
const toKeep = new Set();
|
|
264
|
+
const toDelete = [];
|
|
265
|
+
// Keep recent versions
|
|
266
|
+
if (options.keepRecent) {
|
|
267
|
+
const recentVersions = versions.slice(0, options.keepRecent);
|
|
268
|
+
recentVersions.forEach((v) => toKeep.add(v.version));
|
|
269
|
+
}
|
|
270
|
+
// Keep versions after timestamp
|
|
271
|
+
if (options.keepAfter) {
|
|
272
|
+
versions
|
|
273
|
+
.filter((v) => v.timestamp >= options.keepAfter)
|
|
274
|
+
.forEach((v) => toKeep.add(v.version));
|
|
275
|
+
}
|
|
276
|
+
// Keep tagged versions
|
|
277
|
+
if (options.keepTagged !== false) {
|
|
278
|
+
versions
|
|
279
|
+
.filter((v) => v.tag !== undefined)
|
|
280
|
+
.forEach((v) => toKeep.add(v.version));
|
|
281
|
+
}
|
|
282
|
+
// Build delete list
|
|
283
|
+
for (const version of versions) {
|
|
284
|
+
if (!toKeep.has(version.version)) {
|
|
285
|
+
toDelete.push(version);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Dry run - just return counts
|
|
289
|
+
if (options.dryRun) {
|
|
290
|
+
return {
|
|
291
|
+
deleted: toDelete.length,
|
|
292
|
+
kept: toKeep.size
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// Delete versions
|
|
296
|
+
for (const version of toDelete) {
|
|
297
|
+
await this.versionStorage.deleteVersion(version);
|
|
298
|
+
await this.versionIndex.removeVersion(entityId, version.version, currentBranch);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
deleted: toDelete.length,
|
|
302
|
+
kept: toKeep.size
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get version count for an entity
|
|
307
|
+
*
|
|
308
|
+
* @param entityId Entity ID
|
|
309
|
+
* @returns Number of versions
|
|
310
|
+
*/
|
|
311
|
+
async getVersionCount(entityId) {
|
|
312
|
+
await this.initialize();
|
|
313
|
+
const currentBranch = this.brain.currentBranch;
|
|
314
|
+
return this.versionIndex.getVersionCount(entityId, currentBranch);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check if entity has versions
|
|
318
|
+
*
|
|
319
|
+
* @param entityId Entity ID
|
|
320
|
+
* @returns True if entity has versions
|
|
321
|
+
*/
|
|
322
|
+
async hasVersions(entityId) {
|
|
323
|
+
const count = await this.getVersionCount(entityId);
|
|
324
|
+
return count > 0;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Get latest version of an entity
|
|
328
|
+
*
|
|
329
|
+
* @param entityId Entity ID
|
|
330
|
+
* @returns Latest version metadata or null
|
|
331
|
+
*/
|
|
332
|
+
async getLatest(entityId) {
|
|
333
|
+
await this.initialize();
|
|
334
|
+
const versions = await this.list(entityId, { limit: 1 });
|
|
335
|
+
return versions[0] || null;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Clear all versions for an entity
|
|
339
|
+
*
|
|
340
|
+
* @param entityId Entity ID
|
|
341
|
+
* @returns Number of versions deleted
|
|
342
|
+
*/
|
|
343
|
+
async clear(entityId) {
|
|
344
|
+
await this.initialize();
|
|
345
|
+
const result = await this.prune(entityId, {
|
|
346
|
+
keepRecent: 0,
|
|
347
|
+
keepTagged: false
|
|
348
|
+
});
|
|
349
|
+
return result.deleted;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
//# sourceMappingURL=VersionManager.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VersionStorage - Hybrid Storage for Entity Versions (v5.3.0)
|
|
3
|
+
*
|
|
4
|
+
* Implements content-addressable storage for entity versions:
|
|
5
|
+
* - SHA-256 content hashing for deduplication
|
|
6
|
+
* - Stores versions in .brainy/versions/ directory
|
|
7
|
+
* - Integrates with COW commit system
|
|
8
|
+
* - Space-efficient: Only stores unique content
|
|
9
|
+
*
|
|
10
|
+
* Storage structure:
|
|
11
|
+
* .brainy/versions/
|
|
12
|
+
* ├── entities/
|
|
13
|
+
* │ └── {entityId}/
|
|
14
|
+
* │ └── {contentHash}.json # Entity version data
|
|
15
|
+
* └── index/
|
|
16
|
+
* └── {entityId}.json # Version index (managed by VersionIndex)
|
|
17
|
+
*
|
|
18
|
+
* NO MOCKS - Production implementation
|
|
19
|
+
*/
|
|
20
|
+
import type { NounMetadata } from '../coreTypes.js';
|
|
21
|
+
import type { EntityVersion } from './VersionManager.js';
|
|
22
|
+
/**
|
|
23
|
+
* VersionStorage - Content-addressable version storage
|
|
24
|
+
*/
|
|
25
|
+
export declare class VersionStorage {
|
|
26
|
+
private brain;
|
|
27
|
+
private initialized;
|
|
28
|
+
constructor(brain: any);
|
|
29
|
+
/**
|
|
30
|
+
* Initialize version storage directories
|
|
31
|
+
*/
|
|
32
|
+
initialize(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Calculate SHA-256 hash of entity content
|
|
35
|
+
*
|
|
36
|
+
* Used for content-addressable storage and deduplication
|
|
37
|
+
*
|
|
38
|
+
* @param entity Entity to hash
|
|
39
|
+
* @returns SHA-256 hash (hex string)
|
|
40
|
+
*/
|
|
41
|
+
hashEntity(entity: NounMetadata): string;
|
|
42
|
+
/**
|
|
43
|
+
* Convert entity to stable JSON (sorted keys for consistent hashing)
|
|
44
|
+
*/
|
|
45
|
+
private toStableJson;
|
|
46
|
+
/**
|
|
47
|
+
* Save entity version to content-addressable storage
|
|
48
|
+
*
|
|
49
|
+
* @param version Version metadata
|
|
50
|
+
* @param entity Entity data
|
|
51
|
+
*/
|
|
52
|
+
saveVersion(version: EntityVersion, entity: NounMetadata): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Load entity version from storage
|
|
55
|
+
*
|
|
56
|
+
* @param version Version metadata
|
|
57
|
+
* @returns Entity data or null if not found
|
|
58
|
+
*/
|
|
59
|
+
loadVersion(version: EntityVersion): Promise<NounMetadata | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Delete entity version from storage
|
|
62
|
+
*
|
|
63
|
+
* @param version Version to delete
|
|
64
|
+
*/
|
|
65
|
+
deleteVersion(version: EntityVersion): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Get version storage path
|
|
68
|
+
*
|
|
69
|
+
* @param entityId Entity ID
|
|
70
|
+
* @param contentHash Content hash
|
|
71
|
+
* @returns Storage path
|
|
72
|
+
*/
|
|
73
|
+
private getVersionPath;
|
|
74
|
+
/**
|
|
75
|
+
* Check if content exists in storage
|
|
76
|
+
*
|
|
77
|
+
* @param path Storage path
|
|
78
|
+
* @returns True if exists
|
|
79
|
+
*/
|
|
80
|
+
private contentExists;
|
|
81
|
+
/**
|
|
82
|
+
* Write version data to storage
|
|
83
|
+
*
|
|
84
|
+
* @param path Storage path
|
|
85
|
+
* @param entity Entity data
|
|
86
|
+
*/
|
|
87
|
+
private writeVersionData;
|
|
88
|
+
/**
|
|
89
|
+
* Read version data from storage
|
|
90
|
+
*
|
|
91
|
+
* @param path Storage path
|
|
92
|
+
* @returns Entity data
|
|
93
|
+
*/
|
|
94
|
+
private readVersionData;
|
|
95
|
+
/**
|
|
96
|
+
* Delete version data from storage
|
|
97
|
+
*
|
|
98
|
+
* @param path Storage path
|
|
99
|
+
*/
|
|
100
|
+
private deleteVersionData;
|
|
101
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VersionStorage - Hybrid Storage for Entity Versions (v5.3.0)
|
|
3
|
+
*
|
|
4
|
+
* Implements content-addressable storage for entity versions:
|
|
5
|
+
* - SHA-256 content hashing for deduplication
|
|
6
|
+
* - Stores versions in .brainy/versions/ directory
|
|
7
|
+
* - Integrates with COW commit system
|
|
8
|
+
* - Space-efficient: Only stores unique content
|
|
9
|
+
*
|
|
10
|
+
* Storage structure:
|
|
11
|
+
* .brainy/versions/
|
|
12
|
+
* ├── entities/
|
|
13
|
+
* │ └── {entityId}/
|
|
14
|
+
* │ └── {contentHash}.json # Entity version data
|
|
15
|
+
* └── index/
|
|
16
|
+
* └── {entityId}.json # Version index (managed by VersionIndex)
|
|
17
|
+
*
|
|
18
|
+
* NO MOCKS - Production implementation
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from 'crypto';
|
|
21
|
+
/**
|
|
22
|
+
* VersionStorage - Content-addressable version storage
|
|
23
|
+
*/
|
|
24
|
+
export class VersionStorage {
|
|
25
|
+
constructor(brain) {
|
|
26
|
+
this.initialized = false;
|
|
27
|
+
this.brain = brain;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Initialize version storage directories
|
|
31
|
+
*/
|
|
32
|
+
async initialize() {
|
|
33
|
+
if (this.initialized)
|
|
34
|
+
return;
|
|
35
|
+
// Version storage uses the same storage adapter as the main database
|
|
36
|
+
// Directories are created automatically by the storage adapter
|
|
37
|
+
this.initialized = true;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Calculate SHA-256 hash of entity content
|
|
41
|
+
*
|
|
42
|
+
* Used for content-addressable storage and deduplication
|
|
43
|
+
*
|
|
44
|
+
* @param entity Entity to hash
|
|
45
|
+
* @returns SHA-256 hash (hex string)
|
|
46
|
+
*/
|
|
47
|
+
hashEntity(entity) {
|
|
48
|
+
// Create stable JSON representation (sorted keys)
|
|
49
|
+
const stableJson = this.toStableJson(entity);
|
|
50
|
+
return createHash('sha256').update(stableJson).digest('hex');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Convert entity to stable JSON (sorted keys for consistent hashing)
|
|
54
|
+
*/
|
|
55
|
+
toStableJson(obj) {
|
|
56
|
+
if (obj === null)
|
|
57
|
+
return 'null';
|
|
58
|
+
if (obj === undefined)
|
|
59
|
+
return 'undefined';
|
|
60
|
+
if (typeof obj !== 'object')
|
|
61
|
+
return JSON.stringify(obj);
|
|
62
|
+
if (Array.isArray(obj)) {
|
|
63
|
+
const items = obj.map((item) => this.toStableJson(item));
|
|
64
|
+
return `[${items.join(',')}]`;
|
|
65
|
+
}
|
|
66
|
+
// Sort object keys for stable hashing
|
|
67
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
68
|
+
const pairs = sortedKeys.map((key) => {
|
|
69
|
+
const value = this.toStableJson(obj[key]);
|
|
70
|
+
return `"${key}":${value}`;
|
|
71
|
+
});
|
|
72
|
+
return `{${pairs.join(',')}}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Save entity version to content-addressable storage
|
|
76
|
+
*
|
|
77
|
+
* @param version Version metadata
|
|
78
|
+
* @param entity Entity data
|
|
79
|
+
*/
|
|
80
|
+
async saveVersion(version, entity) {
|
|
81
|
+
await this.initialize();
|
|
82
|
+
// Content-addressable path: .brainy/versions/entities/{entityId}/{contentHash}.json
|
|
83
|
+
const versionPath = this.getVersionPath(version.entityId, version.contentHash);
|
|
84
|
+
// Check if content already exists (deduplication)
|
|
85
|
+
const exists = await this.contentExists(versionPath);
|
|
86
|
+
if (exists) {
|
|
87
|
+
// Content already stored - no need to write again
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Store entity data
|
|
91
|
+
await this.writeVersionData(versionPath, entity);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Load entity version from storage
|
|
95
|
+
*
|
|
96
|
+
* @param version Version metadata
|
|
97
|
+
* @returns Entity data or null if not found
|
|
98
|
+
*/
|
|
99
|
+
async loadVersion(version) {
|
|
100
|
+
await this.initialize();
|
|
101
|
+
const versionPath = this.getVersionPath(version.entityId, version.contentHash);
|
|
102
|
+
try {
|
|
103
|
+
return await this.readVersionData(versionPath);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error(`Failed to load version ${version.version}:`, error);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Delete entity version from storage
|
|
112
|
+
*
|
|
113
|
+
* @param version Version to delete
|
|
114
|
+
*/
|
|
115
|
+
async deleteVersion(version) {
|
|
116
|
+
await this.initialize();
|
|
117
|
+
const versionPath = this.getVersionPath(version.entityId, version.contentHash);
|
|
118
|
+
await this.deleteVersionData(versionPath);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get version storage path
|
|
122
|
+
*
|
|
123
|
+
* @param entityId Entity ID
|
|
124
|
+
* @param contentHash Content hash
|
|
125
|
+
* @returns Storage path
|
|
126
|
+
*/
|
|
127
|
+
getVersionPath(entityId, contentHash) {
|
|
128
|
+
return `versions/entities/${entityId}/${contentHash}.json`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check if content exists in storage
|
|
132
|
+
*
|
|
133
|
+
* @param path Storage path
|
|
134
|
+
* @returns True if exists
|
|
135
|
+
*/
|
|
136
|
+
async contentExists(path) {
|
|
137
|
+
try {
|
|
138
|
+
// Use storage adapter's exists check if available
|
|
139
|
+
const adapter = this.brain.storageAdapter;
|
|
140
|
+
if (adapter && typeof adapter.exists === 'function') {
|
|
141
|
+
return await adapter.exists(path);
|
|
142
|
+
}
|
|
143
|
+
// Fallback: Try to read and catch error
|
|
144
|
+
await this.readVersionData(path);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Write version data to storage
|
|
153
|
+
*
|
|
154
|
+
* @param path Storage path
|
|
155
|
+
* @param entity Entity data
|
|
156
|
+
*/
|
|
157
|
+
async writeVersionData(path, entity) {
|
|
158
|
+
const adapter = this.brain.storageAdapter;
|
|
159
|
+
if (!adapter) {
|
|
160
|
+
throw new Error('Storage adapter not available');
|
|
161
|
+
}
|
|
162
|
+
// Serialize entity data
|
|
163
|
+
const data = JSON.stringify(entity, null, 2);
|
|
164
|
+
// Write to storage using adapter
|
|
165
|
+
if (typeof adapter.writeFile === 'function') {
|
|
166
|
+
await adapter.writeFile(path, data);
|
|
167
|
+
}
|
|
168
|
+
else if (typeof adapter.set === 'function') {
|
|
169
|
+
await adapter.set(path, data);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
throw new Error('Storage adapter does not support write operations');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Read version data from storage
|
|
177
|
+
*
|
|
178
|
+
* @param path Storage path
|
|
179
|
+
* @returns Entity data
|
|
180
|
+
*/
|
|
181
|
+
async readVersionData(path) {
|
|
182
|
+
const adapter = this.brain.storageAdapter;
|
|
183
|
+
if (!adapter) {
|
|
184
|
+
throw new Error('Storage adapter not available');
|
|
185
|
+
}
|
|
186
|
+
// Read from storage using adapter
|
|
187
|
+
let data;
|
|
188
|
+
if (typeof adapter.readFile === 'function') {
|
|
189
|
+
data = await adapter.readFile(path);
|
|
190
|
+
}
|
|
191
|
+
else if (typeof adapter.get === 'function') {
|
|
192
|
+
data = await adapter.get(path);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
throw new Error('Storage adapter does not support read operations');
|
|
196
|
+
}
|
|
197
|
+
// Parse entity data
|
|
198
|
+
return JSON.parse(data);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Delete version data from storage
|
|
202
|
+
*
|
|
203
|
+
* @param path Storage path
|
|
204
|
+
*/
|
|
205
|
+
async deleteVersionData(path) {
|
|
206
|
+
const adapter = this.brain.storageAdapter;
|
|
207
|
+
if (!adapter) {
|
|
208
|
+
throw new Error('Storage adapter not available');
|
|
209
|
+
}
|
|
210
|
+
// Delete from storage using adapter
|
|
211
|
+
if (typeof adapter.deleteFile === 'function') {
|
|
212
|
+
await adapter.deleteFile(path);
|
|
213
|
+
}
|
|
214
|
+
else if (typeof adapter.delete === 'function') {
|
|
215
|
+
await adapter.delete(path);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
throw new Error('Storage adapter does not support delete operations');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=VersionStorage.js.map
|