@soulcraft/brainy 5.7.13 → 5.9.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 +18 -7
- package/README.md +6 -2
- package/dist/augmentations/intelligentImport/handlers/imageHandler.d.ts +12 -16
- package/dist/augmentations/intelligentImport/handlers/imageHandler.js +40 -22
- package/dist/brainy.d.ts +1 -0
- package/dist/brainy.js +137 -126
- package/dist/graph/graphAdjacencyIndex.d.ts +76 -7
- package/dist/graph/graphAdjacencyIndex.js +94 -9
- package/dist/hnsw/typeAwareHNSWIndex.d.ts +3 -3
- package/dist/hnsw/typeAwareHNSWIndex.js +3 -3
- package/dist/query/typeAwareQueryPlanner.d.ts +4 -4
- package/dist/query/typeAwareQueryPlanner.js +4 -4
- package/dist/transaction/Transaction.d.ts +55 -0
- package/dist/transaction/Transaction.js +175 -0
- package/dist/transaction/TransactionManager.d.ts +67 -0
- package/dist/transaction/TransactionManager.js +145 -0
- package/dist/transaction/errors.d.ts +41 -0
- package/dist/transaction/errors.js +66 -0
- package/dist/transaction/index.d.ts +14 -0
- package/dist/transaction/index.js +14 -0
- package/dist/transaction/operations/IndexOperations.d.ts +172 -0
- package/dist/transaction/operations/IndexOperations.js +301 -0
- package/dist/transaction/operations/StorageOperations.d.ts +128 -0
- package/dist/transaction/operations/StorageOperations.js +253 -0
- package/dist/transaction/operations/index.d.ts +10 -0
- package/dist/transaction/operations/index.js +13 -0
- package/dist/transaction/types.d.ts +84 -0
- package/dist/transaction/types.js +8 -0
- package/dist/vfs/VirtualFileSystem.d.ts +16 -0
- package/dist/vfs/VirtualFileSystem.js +138 -48
- package/package.json +3 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction System Types
|
|
3
|
+
*
|
|
4
|
+
* Provides atomicity for Brainy operations - all succeed or all rollback.
|
|
5
|
+
* Prevents partial failures that leave system in inconsistent state.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Transaction state
|
|
9
|
+
*/
|
|
10
|
+
export type TransactionState = 'pending' | 'executing' | 'committed' | 'rolling_back' | 'rolled_back';
|
|
11
|
+
/**
|
|
12
|
+
* Rollback action - undoes an operation
|
|
13
|
+
* Must be idempotent (safe to call multiple times)
|
|
14
|
+
*/
|
|
15
|
+
export type RollbackAction = () => Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Operation that can be executed and rolled back
|
|
18
|
+
*/
|
|
19
|
+
export interface Operation {
|
|
20
|
+
/**
|
|
21
|
+
* Execute the operation
|
|
22
|
+
* @returns Rollback action to undo this operation (or undefined if no rollback needed)
|
|
23
|
+
*/
|
|
24
|
+
execute(): Promise<RollbackAction | undefined>;
|
|
25
|
+
/**
|
|
26
|
+
* Optional: Name of operation for debugging
|
|
27
|
+
*/
|
|
28
|
+
readonly name?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Transaction context passed to user functions
|
|
32
|
+
*/
|
|
33
|
+
export interface TransactionContext {
|
|
34
|
+
/**
|
|
35
|
+
* Add an operation to the transaction
|
|
36
|
+
*/
|
|
37
|
+
addOperation(operation: Operation): void;
|
|
38
|
+
/**
|
|
39
|
+
* Get current transaction state
|
|
40
|
+
*/
|
|
41
|
+
getState(): TransactionState;
|
|
42
|
+
/**
|
|
43
|
+
* Get number of operations in transaction
|
|
44
|
+
*/
|
|
45
|
+
getOperationCount(): number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Function that builds a transaction
|
|
49
|
+
*/
|
|
50
|
+
export type TransactionFunction<T> = (ctx: TransactionContext) => Promise<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Transaction execution result
|
|
53
|
+
*/
|
|
54
|
+
export interface TransactionResult<T> {
|
|
55
|
+
/**
|
|
56
|
+
* Result value from user function
|
|
57
|
+
*/
|
|
58
|
+
value: T;
|
|
59
|
+
/**
|
|
60
|
+
* Number of operations executed
|
|
61
|
+
*/
|
|
62
|
+
operationCount: number;
|
|
63
|
+
/**
|
|
64
|
+
* Execution time in milliseconds
|
|
65
|
+
*/
|
|
66
|
+
executionTimeMs: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Transaction execution options
|
|
70
|
+
*/
|
|
71
|
+
export interface TransactionOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Timeout in milliseconds (default: 30000 = 30 seconds)
|
|
74
|
+
*/
|
|
75
|
+
timeout?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Whether to log transaction execution (default: false)
|
|
78
|
+
*/
|
|
79
|
+
logging?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Maximum number of rollback retry attempts (default: 3)
|
|
82
|
+
*/
|
|
83
|
+
maxRollbackRetries?: number;
|
|
84
|
+
}
|
|
@@ -29,6 +29,7 @@ export declare class VirtualFileSystem implements IVirtualFileSystem {
|
|
|
29
29
|
private watchers;
|
|
30
30
|
private backgroundTimer;
|
|
31
31
|
private mkdirLocks;
|
|
32
|
+
private rootInitPromise;
|
|
32
33
|
constructor(brain?: Brainy);
|
|
33
34
|
/**
|
|
34
35
|
* v5.2.0: Access to BlobStorage for unified file storage
|
|
@@ -46,7 +47,22 @@ export declare class VirtualFileSystem implements IVirtualFileSystem {
|
|
|
46
47
|
* Zero-config: All semantic dimensions work out of the box
|
|
47
48
|
*/
|
|
48
49
|
private registerBuiltInProjections;
|
|
50
|
+
/**
|
|
51
|
+
* v5.8.0: CRITICAL FIX - Prevent duplicate root creation
|
|
52
|
+
* Uses singleton promise pattern to ensure only ONE root initialization
|
|
53
|
+
* happens even with concurrent init() calls
|
|
54
|
+
*/
|
|
49
55
|
private initializeRoot;
|
|
56
|
+
/**
|
|
57
|
+
* v5.8.0: Actual root initialization logic
|
|
58
|
+
* Uses brain.find() with no caching to get consistent results
|
|
59
|
+
*/
|
|
60
|
+
private doInitializeRoot;
|
|
61
|
+
/**
|
|
62
|
+
* v5.8.0: Smart root selection when duplicates exist
|
|
63
|
+
* Selects root with MOST children (not oldest) to preserve user data
|
|
64
|
+
*/
|
|
65
|
+
private selectBestRoot;
|
|
50
66
|
/**
|
|
51
67
|
* Read a file's content
|
|
52
68
|
*/
|
|
@@ -28,6 +28,8 @@ export class VirtualFileSystem {
|
|
|
28
28
|
this.backgroundTimer = null;
|
|
29
29
|
// Mutex for preventing race conditions in directory creation
|
|
30
30
|
this.mkdirLocks = new Map();
|
|
31
|
+
// v5.8.0: Singleton promise for root initialization (prevents duplicate roots)
|
|
32
|
+
this.rootInitPromise = null;
|
|
31
33
|
this.brain = brain || new Brainy();
|
|
32
34
|
this.contentCache = new Map();
|
|
33
35
|
this.statCache = new Map();
|
|
@@ -99,77 +101,154 @@ export class VirtualFileSystem {
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* v5.8.0: CRITICAL FIX - Prevent duplicate root creation
|
|
106
|
+
* Uses singleton promise pattern to ensure only ONE root initialization
|
|
107
|
+
* happens even with concurrent init() calls
|
|
108
|
+
*/
|
|
102
109
|
async initializeRoot() {
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
// If initialization already in progress, wait for it (automatic mutex)
|
|
111
|
+
if (this.rootInitPromise) {
|
|
112
|
+
return await this.rootInitPromise;
|
|
113
|
+
}
|
|
114
|
+
// Start initialization and cache the promise
|
|
115
|
+
this.rootInitPromise = this.doInitializeRoot();
|
|
116
|
+
try {
|
|
117
|
+
const rootId = await this.rootInitPromise;
|
|
118
|
+
return rootId;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// On error, clear promise so retry is possible
|
|
122
|
+
this.rootInitPromise = null;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
// NOTE: On success, we intentionally keep the promise cached
|
|
126
|
+
// This prevents re-initialization and serves as a cache
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* v5.8.0: Actual root initialization logic
|
|
130
|
+
* Uses brain.find() with no caching to get consistent results
|
|
131
|
+
*/
|
|
132
|
+
async doInitializeRoot() {
|
|
133
|
+
// Query for existing roots using brain.find()
|
|
134
|
+
// Use higher limit and no cache to ensure we catch all roots
|
|
135
|
+
const roots = await this.brain.find({
|
|
107
136
|
type: NounType.Collection,
|
|
108
137
|
where: {
|
|
109
|
-
path: '/',
|
|
110
|
-
vfsType: 'directory'
|
|
138
|
+
path: '/',
|
|
139
|
+
vfsType: 'directory'
|
|
111
140
|
},
|
|
112
|
-
limit:
|
|
141
|
+
limit: 50, // Higher limit to catch all possible duplicates
|
|
142
|
+
excludeVFS: false // CRITICAL: Don't exclude VFS entities!
|
|
113
143
|
});
|
|
114
|
-
if (
|
|
115
|
-
//
|
|
116
|
-
if (
|
|
117
|
-
console.warn(`⚠️ Found ${
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Ensure the root entity has proper metadata structure
|
|
129
|
-
const entityMetadata = rootEntity.metadata || rootEntity;
|
|
130
|
-
if (!entityMetadata.vfsType) {
|
|
131
|
-
// Update the root entity with proper metadata
|
|
144
|
+
if (roots.length > 0) {
|
|
145
|
+
// Auto-heal if duplicates exist
|
|
146
|
+
if (roots.length > 1) {
|
|
147
|
+
console.warn(`⚠️ VFS: Found ${roots.length} duplicate root directories`);
|
|
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...`);
|
|
132
158
|
await this.brain.update({
|
|
133
|
-
id:
|
|
159
|
+
id: root.id,
|
|
134
160
|
metadata: {
|
|
161
|
+
...metadata,
|
|
135
162
|
path: '/',
|
|
136
163
|
name: '',
|
|
137
164
|
vfsType: 'directory',
|
|
138
|
-
isVFS: true,
|
|
139
|
-
isVFSEntity: true,
|
|
165
|
+
isVFS: true,
|
|
166
|
+
isVFSEntity: true,
|
|
140
167
|
size: 0,
|
|
141
168
|
permissions: 0o755,
|
|
142
169
|
owner: 'root',
|
|
143
170
|
group: 'root',
|
|
144
171
|
accessed: Date.now(),
|
|
145
|
-
modified: Date.now()
|
|
146
|
-
...entityMetadata // Preserve any existing metadata
|
|
172
|
+
modified: Date.now()
|
|
147
173
|
}
|
|
148
174
|
});
|
|
149
175
|
}
|
|
150
|
-
return
|
|
176
|
+
return root.id;
|
|
151
177
|
}
|
|
152
|
-
//
|
|
178
|
+
// ONLY create root if absolutely zero roots exist
|
|
179
|
+
console.log('VFS: Creating root directory (verified zero roots exist)');
|
|
153
180
|
const root = await this.brain.add({
|
|
154
|
-
data: '/',
|
|
181
|
+
data: '/',
|
|
155
182
|
type: NounType.Collection,
|
|
156
183
|
metadata: {
|
|
157
184
|
path: '/',
|
|
158
185
|
name: '',
|
|
159
186
|
vfsType: 'directory',
|
|
160
|
-
isVFS: true,
|
|
161
|
-
isVFSEntity: true,
|
|
187
|
+
isVFS: true,
|
|
188
|
+
isVFSEntity: true,
|
|
162
189
|
size: 0,
|
|
163
190
|
permissions: 0o755,
|
|
164
191
|
owner: 'root',
|
|
165
192
|
group: 'root',
|
|
166
193
|
accessed: Date.now(),
|
|
167
194
|
modified: Date.now(),
|
|
168
|
-
createdAt: Date.now()
|
|
195
|
+
createdAt: Date.now()
|
|
169
196
|
}
|
|
170
197
|
});
|
|
171
198
|
return root;
|
|
172
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* v5.8.0: Smart root selection when duplicates exist
|
|
202
|
+
* Selects root with MOST children (not oldest) to preserve user data
|
|
203
|
+
*/
|
|
204
|
+
async selectBestRoot(roots) {
|
|
205
|
+
// Count descendants for each root
|
|
206
|
+
const rootStats = await Promise.all(roots.map(async (root) => {
|
|
207
|
+
const children = await this.brain.find({
|
|
208
|
+
where: { parent: root.id },
|
|
209
|
+
limit: 1000 // Cap to prevent huge queries
|
|
210
|
+
});
|
|
211
|
+
// Extract metadata safely from Result object
|
|
212
|
+
const metadata = root.metadata || root;
|
|
213
|
+
return {
|
|
214
|
+
id: root.id,
|
|
215
|
+
createdAt: metadata.createdAt || metadata.modified || 0,
|
|
216
|
+
childCount: children.length,
|
|
217
|
+
sampleChildren: children.slice(0, 3).map(c => {
|
|
218
|
+
const childMeta = c.metadata || c;
|
|
219
|
+
return childMeta.path || childMeta.name || 'unknown';
|
|
220
|
+
})
|
|
221
|
+
};
|
|
222
|
+
}));
|
|
223
|
+
// Sort by child count (DESC), then by creation time (ASC) as tiebreaker
|
|
224
|
+
rootStats.sort((a, b) => {
|
|
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(', ')}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
console.warn(`\n✅ VFS: Selected root ${bestRoot.id.substring(0, 8)} (${bestRoot.childCount} children)`);
|
|
243
|
+
// Suggest cleanup for empty roots
|
|
244
|
+
if (emptyRoots.length > 0) {
|
|
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
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return bestRoot.id;
|
|
251
|
+
}
|
|
173
252
|
// ============= File Operations =============
|
|
174
253
|
/**
|
|
175
254
|
* Read a file's content
|
|
@@ -194,19 +273,30 @@ export class VirtualFileSystem {
|
|
|
194
273
|
if (!entity.metadata.storage?.type || entity.metadata.storage.type !== 'blob') {
|
|
195
274
|
throw new VFSError(VFSErrorCode.EIO, `File has no blob storage: ${path}. Requires v5.2.0+ storage format.`, path, 'readFile');
|
|
196
275
|
}
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
276
|
+
// v5.8.0: CRITICAL FIX - Isolate blob errors from VFS tree corruption
|
|
277
|
+
// Blob read errors MUST NOT cascade to VFS tree structure
|
|
278
|
+
try {
|
|
279
|
+
// Read from BlobStorage (handles decompression automatically)
|
|
280
|
+
const content = await this.blobStorage.read(entity.metadata.storage.hash);
|
|
281
|
+
// Update access time
|
|
282
|
+
await this.updateAccessTime(entityId);
|
|
283
|
+
// Cache the content
|
|
284
|
+
if (options?.cache !== false) {
|
|
285
|
+
this.contentCache.set(path, { data: content, timestamp: Date.now() });
|
|
286
|
+
}
|
|
287
|
+
// Apply encoding if requested
|
|
288
|
+
if (options?.encoding) {
|
|
289
|
+
return Buffer.from(content.toString(options.encoding));
|
|
290
|
+
}
|
|
291
|
+
return content;
|
|
292
|
+
}
|
|
293
|
+
catch (blobError) {
|
|
294
|
+
// Blob error isolated - VFS tree structure remains intact
|
|
295
|
+
const errorMsg = blobError instanceof Error ? blobError.message : String(blobError);
|
|
296
|
+
console.error(`VFS: Cannot read blob for ${path}:`, errorMsg);
|
|
297
|
+
// Throw VFSError (not blob error) - prevents cascading corruption
|
|
298
|
+
throw new VFSError(VFSErrorCode.EIO, `File read failed: ${errorMsg}`, path, 'readFile');
|
|
208
299
|
}
|
|
209
|
-
return content;
|
|
210
300
|
}
|
|
211
301
|
/**
|
|
212
302
|
* Write a file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
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",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"@testcontainers/redis": "^11.5.1",
|
|
162
162
|
"@types/mime": "^3.0.4",
|
|
163
163
|
"@types/node": "^20.11.30",
|
|
164
|
-
"@types/
|
|
164
|
+
"@types/probe-image-size": "^7.2.5",
|
|
165
165
|
"@types/uuid": "^10.0.0",
|
|
166
166
|
"@types/ws": "^8.18.1",
|
|
167
167
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -196,9 +196,9 @@
|
|
|
196
196
|
"mime": "^4.1.0",
|
|
197
197
|
"ora": "^8.2.0",
|
|
198
198
|
"pdfjs-dist": "^4.0.379",
|
|
199
|
+
"probe-image-size": "^7.2.3",
|
|
199
200
|
"prompts": "^2.4.2",
|
|
200
201
|
"roaring-wasm": "^1.1.0",
|
|
201
|
-
"sharp": "^0.33.5",
|
|
202
202
|
"uuid": "^9.0.1",
|
|
203
203
|
"ws": "^8.18.3",
|
|
204
204
|
"xlsx": "^0.18.5"
|