@soulcraft/brainy 5.9.0 → 5.10.1
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 +37 -0
- package/dist/storage/baseStorage.js +7 -19
- package/dist/storage/cow/BlobStorage.js +14 -5
- package/dist/storage/cow/binaryDataCodec.d.ts +68 -0
- package/dist/storage/cow/binaryDataCodec.js +103 -0
- package/dist/vfs/VirtualFileSystem.d.ts +16 -5
- package/dist/vfs/VirtualFileSystem.js +103 -111
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [5.10.1](https://github.com/soulcraftlabs/brainy/compare/v5.10.0...v5.10.1) (2025-11-14)
|
|
6
|
+
|
|
7
|
+
### 🚨 CRITICAL BUG FIX - Blob Integrity Regression
|
|
8
|
+
|
|
9
|
+
**v5.10.0 regressed the v5.7.2 blob integrity bug, causing 100% VFS file read failure. This hotfix restores functionality with defense-in-depth architecture.**
|
|
10
|
+
|
|
11
|
+
### Bug Description
|
|
12
|
+
v5.10.0 reintroduced a critical bug where `BlobStorage.read()` was hashing wrapped binary data instead of unwrapped content, causing all blob integrity checks to fail:
|
|
13
|
+
- **Symptom**: `Blob integrity check failed: <hash>` errors on every VFS file read
|
|
14
|
+
- **Root Cause**: Missing defense-in-depth unwrap verification in `BlobStorage.read()`
|
|
15
|
+
- **Impact**: 100% failure rate for VFS file operations in Workshop application
|
|
16
|
+
|
|
17
|
+
### The Fix (v5.10.1)
|
|
18
|
+
1. **Defense-in-Depth Unwrapping**: Added unwrap verification in `BlobStorage.read()` before hash check
|
|
19
|
+
2. **DRY Architecture**: Created `binaryDataCodec.ts` as single source of truth for wrap/unwrap logic
|
|
20
|
+
3. **Metadata Unwrapping**: Fixed metadata parsing to handle wrapped format
|
|
21
|
+
4. **Comprehensive Tests**: Added 3 regression tests using `TestWrappingAdapter`
|
|
22
|
+
|
|
23
|
+
### Changes
|
|
24
|
+
- **NEW**: `src/storage/cow/binaryDataCodec.ts` - Single source of truth for binary data encoding/decoding
|
|
25
|
+
- **FIXED**: `src/storage/cow/BlobStorage.ts` - Unwraps data and metadata before verification (lines 314, 342)
|
|
26
|
+
- **REFACTORED**: `src/storage/baseStorage.ts` - Uses shared binaryDataCodec utilities (lines 332, 340)
|
|
27
|
+
- **ADDED**: `tests/helpers/TestWrappingAdapter.ts` - Real wrapping adapter for testing
|
|
28
|
+
- **ADDED**: 3 regression tests in `tests/unit/storage/cow/BlobStorage.test.ts`
|
|
29
|
+
|
|
30
|
+
### Architecture Improvements
|
|
31
|
+
- ✅ **Defense-in-Depth**: Unwrap at BOTH adapter layer (v5.7.5) and blob layer (v5.10.1)
|
|
32
|
+
- ✅ **DRY Principle**: All wrap/unwrap operations use shared `binaryDataCodec.ts`
|
|
33
|
+
- ✅ **Works Across ALL 8 Storage Adapters**: FileSystem, Memory, S3, GCS, Azure, R2, OPFS, Historical
|
|
34
|
+
- ✅ **Prevents Future Regressions**: Real wrapping tests catch this bug class
|
|
35
|
+
|
|
36
|
+
### Related Issues
|
|
37
|
+
- v5.7.2: Original blob integrity bug - hashed wrapper instead of content
|
|
38
|
+
- v5.7.5: First fix - added unwrap to COW adapter (necessary but insufficient)
|
|
39
|
+
- v5.10.0: Regression - missing defense-in-depth in BlobStorage layer
|
|
40
|
+
- v5.10.1: Complete fix - defense-in-depth + DRY architecture + comprehensive tests
|
|
41
|
+
|
|
5
42
|
### [5.9.0](https://github.com/soulcraftlabs/brainy/compare/v5.8.0...v5.9.0) (2025-11-14)
|
|
6
43
|
|
|
7
44
|
- fix: resolve VFS tree corruption from blob errors (v5.8.0) (93d2d70)
|
|
@@ -10,6 +10,7 @@ import { getShardIdFromUuid } from './sharding.js';
|
|
|
10
10
|
import { RefManager } from './cow/RefManager.js';
|
|
11
11
|
import { BlobStorage } from './cow/BlobStorage.js';
|
|
12
12
|
import { CommitLog } from './cow/CommitLog.js';
|
|
13
|
+
import { unwrapBinaryData, wrapBinaryData } from './cow/binaryDataCodec.js';
|
|
13
14
|
import { prodLog } from '../utils/logger.js';
|
|
14
15
|
// Clean directory structure (v4.7.2+)
|
|
15
16
|
// All storage adapters use this consistent structure
|
|
@@ -234,32 +235,19 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
234
235
|
if (data === null) {
|
|
235
236
|
return undefined;
|
|
236
237
|
}
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
return data;
|
|
240
|
-
}
|
|
241
|
-
// v5.7.5: Unwrap binary data stored as {_binary: true, data: "base64..."}
|
|
238
|
+
// v5.7.5/v5.10.1: Use shared binaryDataCodec utility (single source of truth)
|
|
239
|
+
// Unwraps binary data stored as {_binary: true, data: "base64..."}
|
|
242
240
|
// Fixes "Blob integrity check failed" - hash must be calculated on original content
|
|
243
|
-
|
|
244
|
-
return Buffer.from(data.data, 'base64');
|
|
245
|
-
}
|
|
246
|
-
return Buffer.from(JSON.stringify(data));
|
|
241
|
+
return unwrapBinaryData(data);
|
|
247
242
|
}
|
|
248
243
|
catch (error) {
|
|
249
244
|
return undefined;
|
|
250
245
|
}
|
|
251
246
|
},
|
|
252
247
|
put: async (key, data) => {
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// Try to parse as JSON first (for metadata)
|
|
257
|
-
obj = JSON.parse(data.toString());
|
|
258
|
-
}
|
|
259
|
-
catch {
|
|
260
|
-
// Not JSON, store as binary (base64 encoded for JSON storage)
|
|
261
|
-
obj = { _binary: true, data: data.toString('base64') };
|
|
262
|
-
}
|
|
248
|
+
// v5.10.1: Use shared binaryDataCodec utility (single source of truth)
|
|
249
|
+
// Wraps binary data or parses JSON for storage
|
|
250
|
+
const obj = wrapBinaryData(data);
|
|
263
251
|
await this.writeObjectToPath(`_cow/${key}`, obj);
|
|
264
252
|
},
|
|
265
253
|
delete: async (key) => {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from 'crypto';
|
|
17
17
|
import { NULL_HASH, isNullHash } from './constants.js';
|
|
18
|
+
import { unwrapBinaryData } from './binaryDataCodec.js';
|
|
18
19
|
/**
|
|
19
20
|
* State-of-the-art content-addressable blob storage
|
|
20
21
|
*
|
|
@@ -207,7 +208,10 @@ export class BlobStorage {
|
|
|
207
208
|
metadataBuffer = await this.adapter.get(`${tryPrefix}-meta:${hash}`);
|
|
208
209
|
if (metadataBuffer) {
|
|
209
210
|
prefix = tryPrefix;
|
|
210
|
-
metadata
|
|
211
|
+
// v5.10.1: Unwrap metadata before parsing (defense-in-depth)
|
|
212
|
+
// Metadata should be JSON, but adapter might return wrapped format
|
|
213
|
+
const unwrappedMetadata = unwrapBinaryData(metadataBuffer);
|
|
214
|
+
metadata = JSON.parse(unwrappedMetadata.toString());
|
|
211
215
|
break;
|
|
212
216
|
}
|
|
213
217
|
}
|
|
@@ -227,15 +231,20 @@ export class BlobStorage {
|
|
|
227
231
|
}
|
|
228
232
|
finalData = await this.zstdDecompress(data);
|
|
229
233
|
}
|
|
230
|
-
//
|
|
231
|
-
|
|
234
|
+
// v5.10.1: Defense-in-depth unwrap (CRITICAL FIX for blob integrity regression)
|
|
235
|
+
// Even though COW adapter should unwrap (v5.7.5), verify it happened and re-unwrap if needed
|
|
236
|
+
// This prevents "Blob integrity check failed" errors if adapter returns wrapped data
|
|
237
|
+
// Uses shared binaryDataCodec utility (single source of truth for unwrap logic)
|
|
238
|
+
const unwrappedData = unwrapBinaryData(finalData);
|
|
239
|
+
// Verify hash (on unwrapped data)
|
|
240
|
+
if (!options.skipVerification && BlobStorage.hash(unwrappedData) !== hash) {
|
|
232
241
|
throw new Error(`Blob integrity check failed: ${hash}`);
|
|
233
242
|
}
|
|
234
243
|
// Add to cache (only if not skipped)
|
|
235
244
|
if (!options.skipCache) {
|
|
236
|
-
this.addToCache(hash,
|
|
245
|
+
this.addToCache(hash, unwrappedData, metadata);
|
|
237
246
|
}
|
|
238
|
-
return
|
|
247
|
+
return unwrappedData;
|
|
239
248
|
}
|
|
240
249
|
/**
|
|
241
250
|
* Check if blob exists
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary Data Codec: Single Source of Truth for Wrap/Unwrap Operations
|
|
3
|
+
*
|
|
4
|
+
* This module provides the ONLY implementation of binary data encoding/decoding
|
|
5
|
+
* used across all storage adapters and blob storage.
|
|
6
|
+
*
|
|
7
|
+
* Design Principles:
|
|
8
|
+
* - DRY: One implementation, used everywhere
|
|
9
|
+
* - Single Responsibility: Only handles binary ↔ JSON conversion
|
|
10
|
+
* - Type-Safe: Proper TypeScript types
|
|
11
|
+
* - Defensive: Handles all edge cases
|
|
12
|
+
*
|
|
13
|
+
* Used by:
|
|
14
|
+
* - BaseStorage COW adapter (write/read operations)
|
|
15
|
+
* - BlobStorage (defense-in-depth verification)
|
|
16
|
+
* - All storage adapters (via BaseStorage)
|
|
17
|
+
*
|
|
18
|
+
* @module storage/cow/binaryDataCodec
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Wrapped binary data format
|
|
22
|
+
* Used when storing binary data in JSON-based storage
|
|
23
|
+
*/
|
|
24
|
+
export interface WrappedBinaryData {
|
|
25
|
+
_binary: true;
|
|
26
|
+
data: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if data is wrapped binary format
|
|
30
|
+
*/
|
|
31
|
+
export declare function isWrappedBinary(data: any): data is WrappedBinaryData;
|
|
32
|
+
/**
|
|
33
|
+
* Unwrap binary data from JSON wrapper
|
|
34
|
+
*
|
|
35
|
+
* This is the SINGLE SOURCE OF TRUTH for unwrapping binary data.
|
|
36
|
+
* All storage operations MUST use this function.
|
|
37
|
+
*
|
|
38
|
+
* Handles:
|
|
39
|
+
* - Buffer → Buffer (pass-through)
|
|
40
|
+
* - {_binary: true, data: "base64..."} → Buffer (unwrap)
|
|
41
|
+
* - Plain object → Buffer (JSON stringify)
|
|
42
|
+
* - Other types → Error
|
|
43
|
+
*
|
|
44
|
+
* @param data - Data to unwrap (may be Buffer, wrapped object, or plain object)
|
|
45
|
+
* @returns Unwrapped Buffer
|
|
46
|
+
* @throws Error if data type is invalid
|
|
47
|
+
*/
|
|
48
|
+
export declare function unwrapBinaryData(data: any): Buffer;
|
|
49
|
+
/**
|
|
50
|
+
* Wrap binary data for JSON storage
|
|
51
|
+
*
|
|
52
|
+
* This is the SINGLE SOURCE OF TRUTH for wrapping binary data.
|
|
53
|
+
* All storage operations MUST use this function.
|
|
54
|
+
*
|
|
55
|
+
* @param data - Buffer to wrap
|
|
56
|
+
* @returns Wrapped object or parsed JSON object
|
|
57
|
+
*/
|
|
58
|
+
export declare function wrapBinaryData(data: Buffer): any;
|
|
59
|
+
/**
|
|
60
|
+
* Ensure data is a Buffer
|
|
61
|
+
*
|
|
62
|
+
* Convenience function that combines type checking and unwrapping.
|
|
63
|
+
* Use this when you need to ensure you have a Buffer.
|
|
64
|
+
*
|
|
65
|
+
* @param data - Data that should be or can be converted to Buffer
|
|
66
|
+
* @returns Buffer
|
|
67
|
+
*/
|
|
68
|
+
export declare function ensureBuffer(data: any): Buffer;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary Data Codec: Single Source of Truth for Wrap/Unwrap Operations
|
|
3
|
+
*
|
|
4
|
+
* This module provides the ONLY implementation of binary data encoding/decoding
|
|
5
|
+
* used across all storage adapters and blob storage.
|
|
6
|
+
*
|
|
7
|
+
* Design Principles:
|
|
8
|
+
* - DRY: One implementation, used everywhere
|
|
9
|
+
* - Single Responsibility: Only handles binary ↔ JSON conversion
|
|
10
|
+
* - Type-Safe: Proper TypeScript types
|
|
11
|
+
* - Defensive: Handles all edge cases
|
|
12
|
+
*
|
|
13
|
+
* Used by:
|
|
14
|
+
* - BaseStorage COW adapter (write/read operations)
|
|
15
|
+
* - BlobStorage (defense-in-depth verification)
|
|
16
|
+
* - All storage adapters (via BaseStorage)
|
|
17
|
+
*
|
|
18
|
+
* @module storage/cow/binaryDataCodec
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Check if data is wrapped binary format
|
|
22
|
+
*/
|
|
23
|
+
export function isWrappedBinary(data) {
|
|
24
|
+
return (typeof data === 'object' &&
|
|
25
|
+
data !== null &&
|
|
26
|
+
data._binary === true &&
|
|
27
|
+
typeof data.data === 'string');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Unwrap binary data from JSON wrapper
|
|
31
|
+
*
|
|
32
|
+
* This is the SINGLE SOURCE OF TRUTH for unwrapping binary data.
|
|
33
|
+
* All storage operations MUST use this function.
|
|
34
|
+
*
|
|
35
|
+
* Handles:
|
|
36
|
+
* - Buffer → Buffer (pass-through)
|
|
37
|
+
* - {_binary: true, data: "base64..."} → Buffer (unwrap)
|
|
38
|
+
* - Plain object → Buffer (JSON stringify)
|
|
39
|
+
* - Other types → Error
|
|
40
|
+
*
|
|
41
|
+
* @param data - Data to unwrap (may be Buffer, wrapped object, or plain object)
|
|
42
|
+
* @returns Unwrapped Buffer
|
|
43
|
+
* @throws Error if data type is invalid
|
|
44
|
+
*/
|
|
45
|
+
export function unwrapBinaryData(data) {
|
|
46
|
+
// Case 1: Already a Buffer (no unwrapping needed)
|
|
47
|
+
if (Buffer.isBuffer(data)) {
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
// Case 2: Wrapped binary data {_binary: true, data: "base64..."}
|
|
51
|
+
if (isWrappedBinary(data)) {
|
|
52
|
+
return Buffer.from(data.data, 'base64');
|
|
53
|
+
}
|
|
54
|
+
// Case 3: Plain object (shouldn't happen for binary blobs, but handle gracefully)
|
|
55
|
+
if (typeof data === 'object' && data !== null) {
|
|
56
|
+
return Buffer.from(JSON.stringify(data));
|
|
57
|
+
}
|
|
58
|
+
// Case 4: String (convert to Buffer)
|
|
59
|
+
if (typeof data === 'string') {
|
|
60
|
+
return Buffer.from(data);
|
|
61
|
+
}
|
|
62
|
+
// Case 5: Invalid type
|
|
63
|
+
throw new Error(`Invalid data type for unwrap: ${typeof data}. ` +
|
|
64
|
+
`Expected Buffer or {_binary: true, data: "base64..."}`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wrap binary data for JSON storage
|
|
68
|
+
*
|
|
69
|
+
* This is the SINGLE SOURCE OF TRUTH for wrapping binary data.
|
|
70
|
+
* All storage operations MUST use this function.
|
|
71
|
+
*
|
|
72
|
+
* @param data - Buffer to wrap
|
|
73
|
+
* @returns Wrapped object or parsed JSON object
|
|
74
|
+
*/
|
|
75
|
+
export function wrapBinaryData(data) {
|
|
76
|
+
// Try to parse as JSON first (for metadata, trees, commits)
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(data.toString());
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Not JSON - wrap as binary data
|
|
82
|
+
return {
|
|
83
|
+
_binary: true,
|
|
84
|
+
data: data.toString('base64')
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Ensure data is a Buffer
|
|
90
|
+
*
|
|
91
|
+
* Convenience function that combines type checking and unwrapping.
|
|
92
|
+
* Use this when you need to ensure you have a Buffer.
|
|
93
|
+
*
|
|
94
|
+
* @param data - Data that should be or can be converted to Buffer
|
|
95
|
+
* @returns Buffer
|
|
96
|
+
*/
|
|
97
|
+
export function ensureBuffer(data) {
|
|
98
|
+
if (Buffer.isBuffer(data)) {
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
return unwrapBinaryData(data);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=binaryDataCodec.js.map
|
|
@@ -30,6 +30,7 @@ export declare class VirtualFileSystem implements IVirtualFileSystem {
|
|
|
30
30
|
private backgroundTimer;
|
|
31
31
|
private mkdirLocks;
|
|
32
32
|
private rootInitPromise;
|
|
33
|
+
private static readonly VFS_ROOT_ID;
|
|
33
34
|
constructor(brain?: Brainy);
|
|
34
35
|
/**
|
|
35
36
|
* v5.2.0: Access to BlobStorage for unified file storage
|
|
@@ -54,15 +55,25 @@ export declare class VirtualFileSystem implements IVirtualFileSystem {
|
|
|
54
55
|
*/
|
|
55
56
|
private initializeRoot;
|
|
56
57
|
/**
|
|
57
|
-
* v5.
|
|
58
|
-
* Uses
|
|
58
|
+
* v5.10.0: Atomic root initialization with fixed ID
|
|
59
|
+
* Uses deterministic ID to prevent duplicates across all VFS instances
|
|
60
|
+
*
|
|
61
|
+
* ARCHITECTURAL FIX: Instead of query-then-create (race condition),
|
|
62
|
+
* we use a fixed ID so storage-level uniqueness prevents duplicates.
|
|
59
63
|
*/
|
|
60
64
|
private doInitializeRoot;
|
|
61
65
|
/**
|
|
62
|
-
* v5.
|
|
63
|
-
*
|
|
66
|
+
* v5.10.0: Get standard root metadata
|
|
67
|
+
* Centralized to ensure consistency
|
|
64
68
|
*/
|
|
65
|
-
private
|
|
69
|
+
private getRootMetadata;
|
|
70
|
+
/**
|
|
71
|
+
* v5.10.0: Cleanup old UUID-based VFS roots (migration from v5.9.0)
|
|
72
|
+
* Called during init to remove duplicate roots created before fixed-ID fix
|
|
73
|
+
*
|
|
74
|
+
* This is a one-time migration helper that can be removed in future versions.
|
|
75
|
+
*/
|
|
76
|
+
private cleanupOldRoots;
|
|
66
77
|
/**
|
|
67
78
|
* Read a file's content
|
|
68
79
|
*/
|
|
@@ -61,6 +61,8 @@ export class VirtualFileSystem {
|
|
|
61
61
|
// Removed brain.init() check to prevent infinite recursion
|
|
62
62
|
// Create or find root entity
|
|
63
63
|
this.rootEntityId = await this.initializeRoot();
|
|
64
|
+
// v5.10.0: Clean up old UUID-based roots from v5.9.0 (one-time migration)
|
|
65
|
+
await this.cleanupOldRoots();
|
|
64
66
|
// Initialize projection registry with auto-discovery of built-in projections
|
|
65
67
|
this.projectionRegistry = new ProjectionRegistry();
|
|
66
68
|
this.registerBuiltInProjections();
|
|
@@ -126,128 +128,115 @@ export class VirtualFileSystem {
|
|
|
126
128
|
// This prevents re-initialization and serves as a cache
|
|
127
129
|
}
|
|
128
130
|
/**
|
|
129
|
-
* v5.
|
|
130
|
-
* Uses
|
|
131
|
+
* v5.10.0: Atomic root initialization with fixed ID
|
|
132
|
+
* Uses deterministic ID to prevent duplicates across all VFS instances
|
|
133
|
+
*
|
|
134
|
+
* ARCHITECTURAL FIX: Instead of query-then-create (race condition),
|
|
135
|
+
* we use a fixed ID so storage-level uniqueness prevents duplicates.
|
|
131
136
|
*/
|
|
132
137
|
async doInitializeRoot() {
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.warn(`⚠️ VFS: Auto-selecting best root (most children)`);
|
|
149
|
-
return await this.selectBestRoot(roots);
|
|
150
|
-
}
|
|
151
|
-
// Single root - verify metadata and return
|
|
152
|
-
const root = roots[0];
|
|
153
|
-
// Extract metadata safely from Result object
|
|
154
|
-
const metadata = root.metadata || root;
|
|
155
|
-
// Ensure proper metadata structure
|
|
156
|
-
if (!metadata.vfsType || metadata.vfsType !== 'directory') {
|
|
157
|
-
console.warn(`⚠️ VFS: Root metadata incomplete, repairing...`);
|
|
158
|
-
await this.brain.update({
|
|
159
|
-
id: root.id,
|
|
160
|
-
metadata: {
|
|
161
|
-
...metadata,
|
|
162
|
-
path: '/',
|
|
163
|
-
name: '',
|
|
164
|
-
vfsType: 'directory',
|
|
165
|
-
isVFS: true,
|
|
166
|
-
isVFSEntity: true,
|
|
167
|
-
size: 0,
|
|
168
|
-
permissions: 0o755,
|
|
169
|
-
owner: 'root',
|
|
170
|
-
group: 'root',
|
|
171
|
-
accessed: Date.now(),
|
|
172
|
-
modified: Date.now()
|
|
173
|
-
}
|
|
174
|
-
});
|
|
138
|
+
const rootId = VirtualFileSystem.VFS_ROOT_ID;
|
|
139
|
+
// Try to get existing root by fixed ID (O(1) lookup, not query)
|
|
140
|
+
try {
|
|
141
|
+
const existingRoot = await this.brain.get(rootId);
|
|
142
|
+
if (existingRoot) {
|
|
143
|
+
// Root exists - verify metadata is correct
|
|
144
|
+
const metadata = existingRoot.metadata || existingRoot;
|
|
145
|
+
if (!metadata.vfsType || metadata.vfsType !== 'directory') {
|
|
146
|
+
console.warn('⚠️ VFS: Root metadata incomplete, repairing...');
|
|
147
|
+
await this.brain.update({
|
|
148
|
+
id: rootId,
|
|
149
|
+
metadata: this.getRootMetadata()
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return rootId;
|
|
175
153
|
}
|
|
176
|
-
return root.id;
|
|
177
154
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
155
|
+
catch (error) {
|
|
156
|
+
// Root doesn't exist yet - proceed to creation
|
|
157
|
+
}
|
|
158
|
+
// Create root with fixed ID (idempotent - fails gracefully if exists)
|
|
159
|
+
try {
|
|
160
|
+
console.log('VFS: Creating root directory (fixed ID: 00000000-0000-0000-0000-000000000000)');
|
|
161
|
+
await this.brain.add({
|
|
162
|
+
id: rootId, // Fixed ID - storage ensures uniqueness
|
|
163
|
+
data: '/',
|
|
164
|
+
type: NounType.Collection,
|
|
165
|
+
metadata: this.getRootMetadata()
|
|
166
|
+
});
|
|
167
|
+
return rootId;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
// If creation failed due to duplicate ID, another instance created it
|
|
171
|
+
// This is normal in concurrent scenarios - just return the fixed ID
|
|
172
|
+
const errorMsg = error?.message?.toLowerCase() || '';
|
|
173
|
+
if (errorMsg.includes('already exists') ||
|
|
174
|
+
errorMsg.includes('duplicate') ||
|
|
175
|
+
errorMsg.includes('eexist')) {
|
|
176
|
+
console.log('VFS: Root already created by another instance, using existing');
|
|
177
|
+
return rootId;
|
|
178
|
+
}
|
|
179
|
+
// Unexpected error
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* v5.10.0: Get standard root metadata
|
|
185
|
+
* Centralized to ensure consistency
|
|
186
|
+
*/
|
|
187
|
+
getRootMetadata() {
|
|
188
|
+
return {
|
|
189
|
+
path: '/',
|
|
190
|
+
name: '',
|
|
191
|
+
vfsType: 'directory',
|
|
192
|
+
isVFS: true,
|
|
193
|
+
isVFSEntity: true,
|
|
194
|
+
size: 0,
|
|
195
|
+
permissions: 0o755,
|
|
196
|
+
owner: 'root',
|
|
197
|
+
group: 'root',
|
|
198
|
+
accessed: Date.now(),
|
|
199
|
+
modified: Date.now()
|
|
200
|
+
};
|
|
199
201
|
}
|
|
200
202
|
/**
|
|
201
|
-
* v5.
|
|
202
|
-
*
|
|
203
|
+
* v5.10.0: Cleanup old UUID-based VFS roots (migration from v5.9.0)
|
|
204
|
+
* Called during init to remove duplicate roots created before fixed-ID fix
|
|
205
|
+
*
|
|
206
|
+
* This is a one-time migration helper that can be removed in future versions.
|
|
203
207
|
*/
|
|
204
|
-
async
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
async cleanupOldRoots() {
|
|
209
|
+
try {
|
|
210
|
+
// Find any old VFS roots with UUID-based IDs (not our fixed ID)
|
|
211
|
+
const oldRoots = await this.brain.find({
|
|
212
|
+
type: NounType.Collection,
|
|
213
|
+
where: {
|
|
214
|
+
path: '/',
|
|
215
|
+
vfsType: 'directory'
|
|
216
|
+
},
|
|
217
|
+
limit: 100,
|
|
218
|
+
excludeVFS: false
|
|
210
219
|
});
|
|
211
|
-
//
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (b.childCount !== a.childCount) {
|
|
226
|
-
return b.childCount - a.childCount; // Most children first
|
|
227
|
-
}
|
|
228
|
-
return a.createdAt - b.createdAt; // Oldest first (tiebreaker)
|
|
229
|
-
});
|
|
230
|
-
const bestRoot = rootStats[0];
|
|
231
|
-
const emptyRoots = rootStats.filter(r => r.childCount === 0);
|
|
232
|
-
// Log detailed statistics
|
|
233
|
-
console.warn(`\n📊 VFS Root Statistics:`);
|
|
234
|
-
for (const stat of rootStats) {
|
|
235
|
-
const shortId = stat.id.substring(0, 8);
|
|
236
|
-
const created = stat.createdAt ? new Date(stat.createdAt).toISOString() : 'unknown';
|
|
237
|
-
console.warn(` Root ${shortId}: ${stat.childCount} children, created ${created}`);
|
|
238
|
-
if (stat.sampleChildren.length > 0) {
|
|
239
|
-
console.warn(` Sample: ${stat.sampleChildren.join(', ')}`);
|
|
220
|
+
// Filter out our fixed-ID root
|
|
221
|
+
const duplicates = oldRoots.filter(r => r.id !== VirtualFileSystem.VFS_ROOT_ID);
|
|
222
|
+
if (duplicates.length > 0) {
|
|
223
|
+
console.log(`VFS: Found ${duplicates.length} old UUID-based root(s) from v5.9.0, cleaning up...`);
|
|
224
|
+
for (const duplicate of duplicates) {
|
|
225
|
+
try {
|
|
226
|
+
await this.brain.delete(duplicate.id);
|
|
227
|
+
console.log(`VFS: Deleted old root ${duplicate.id.substring(0, 8)}`);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
console.warn(`VFS: Failed to delete old root ${duplicate.id}:`, error);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
console.log('VFS: Cleanup complete - all old roots removed');
|
|
240
234
|
}
|
|
241
235
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
console.warn(`\n💡 VFS: Found ${emptyRoots.length} empty duplicate root(s), suggest cleanup:`);
|
|
246
|
-
for (const empty of emptyRoots) {
|
|
247
|
-
console.warn(` await brain.delete('${empty.id}') // Empty VFS root`);
|
|
248
|
-
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
// Non-critical error - log and continue
|
|
238
|
+
console.warn('VFS: Cleanup of old roots failed (non-critical):', error);
|
|
249
239
|
}
|
|
250
|
-
return bestRoot.id;
|
|
251
240
|
}
|
|
252
241
|
// ============= File Operations =============
|
|
253
242
|
/**
|
|
@@ -2140,4 +2129,7 @@ export class VirtualFileSystem {
|
|
|
2140
2129
|
return await this.pathResolver.resolve(normalizedPath);
|
|
2141
2130
|
}
|
|
2142
2131
|
}
|
|
2132
|
+
// v5.10.0: Fixed VFS root ID (prevents duplicates across instances)
|
|
2133
|
+
// Uses deterministic UUID format for storage compatibility
|
|
2134
|
+
VirtualFileSystem.VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000';
|
|
2143
2135
|
//# sourceMappingURL=VirtualFileSystem.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.1",
|
|
4
4
|
"description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. Stage 3 CANONICAL: 42 nouns × 127 verbs covering 96-97% of all human knowledge.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|