@soulcraft/brainy 5.0.0 → 5.1.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.
@@ -124,22 +124,41 @@ export class BaseStorage extends BaseStorageAdapter {
124
124
  await this.init();
125
125
  }
126
126
  }
127
+ /**
128
+ * Lightweight COW enablement - just enables branch-scoped paths
129
+ * Called during init() to ensure all data is stored with branch prefixes from the start
130
+ * RefManager/BlobStorage/CommitLog are lazy-initialized on first fork()
131
+ * @param branch - Branch name to use (default: 'main')
132
+ */
133
+ enableCOWLightweight(branch = 'main') {
134
+ if (this.cowEnabled) {
135
+ return;
136
+ }
137
+ this.currentBranch = branch;
138
+ this.cowEnabled = true;
139
+ // RefManager/BlobStorage/CommitLog remain undefined until first fork()
140
+ }
127
141
  /**
128
142
  * Initialize COW (Copy-on-Write) support
129
143
  * Creates RefManager and BlobStorage for instant fork() capability
130
144
  *
145
+ * v5.0.1: Now called automatically by storageFactory (zero-config)
146
+ *
131
147
  * @param options - COW initialization options
132
148
  * @param options.branch - Initial branch name (default: 'main')
133
149
  * @param options.enableCompression - Enable zstd compression for blobs (default: true)
134
150
  * @returns Promise that resolves when COW is initialized
135
151
  */
136
152
  async initializeCOW(options) {
137
- if (this.cowEnabled) {
138
- // Already initialized
153
+ // Check if RefManager already initialized (full COW setup complete)
154
+ if (this.refManager) {
139
155
  return;
140
156
  }
141
- // Set current branch
142
- this.currentBranch = options?.branch || 'main';
157
+ // Enable lightweight COW if not already enabled
158
+ if (!this.cowEnabled) {
159
+ this.currentBranch = options?.branch || 'main';
160
+ this.cowEnabled = true;
161
+ }
143
162
  // Create COWStorageAdapter bridge
144
163
  // This adapts BaseStorage's methods to the simple key-value interface
145
164
  const cowAdapter = {
@@ -227,6 +246,119 @@ export class BaseStorage extends BaseStorageAdapter {
227
246
  }
228
247
  this.cowEnabled = true;
229
248
  }
249
+ /**
250
+ * Resolve branch-scoped path for COW isolation
251
+ * @protected - Available to subclasses for COW implementation
252
+ */
253
+ resolveBranchPath(basePath, branch) {
254
+ if (!this.cowEnabled) {
255
+ return basePath; // COW disabled, use direct path
256
+ }
257
+ const targetBranch = branch || this.currentBranch || 'main';
258
+ // Branch-scoped path: branches/<branch>/<basePath>
259
+ return `branches/${targetBranch}/${basePath}`;
260
+ }
261
+ /**
262
+ * Write object to branch-specific path (COW layer)
263
+ * @protected - Available to subclasses for COW implementation
264
+ */
265
+ async writeObjectToBranch(path, data, branch) {
266
+ const branchPath = this.resolveBranchPath(path, branch);
267
+ return this.writeObjectToPath(branchPath, data);
268
+ }
269
+ /**
270
+ * Read object with inheritance from parent branches (COW layer)
271
+ * Tries current branch first, then walks commit history
272
+ * @protected - Available to subclasses for COW implementation
273
+ */
274
+ async readWithInheritance(path, branch) {
275
+ if (!this.cowEnabled) {
276
+ // COW disabled, direct read
277
+ return this.readObjectFromPath(path);
278
+ }
279
+ const targetBranch = branch || this.currentBranch || 'main';
280
+ // Try current branch first
281
+ const branchPath = this.resolveBranchPath(path, targetBranch);
282
+ let data = await this.readObjectFromPath(branchPath);
283
+ if (data !== null) {
284
+ return data; // Found in current branch
285
+ }
286
+ // Not in branch, check if we're on main (no inheritance needed)
287
+ if (targetBranch === 'main') {
288
+ return null;
289
+ }
290
+ // Not in branch, walk commit history to find in parent
291
+ if (this.refManager && this.commitLog) {
292
+ try {
293
+ const commitHash = await this.refManager.resolveRef(targetBranch);
294
+ if (commitHash) {
295
+ // Walk parent commits until we find the data
296
+ for await (const commit of this.commitLog.walk(commitHash)) {
297
+ // Try reading from parent's branch path
298
+ const parentBranch = commit.metadata?.branch || 'main';
299
+ if (parentBranch === targetBranch)
300
+ continue; // Skip self
301
+ const parentPath = this.resolveBranchPath(path, parentBranch);
302
+ data = await this.readObjectFromPath(parentPath);
303
+ if (data !== null) {
304
+ return data; // Found in ancestor
305
+ }
306
+ }
307
+ }
308
+ }
309
+ catch (error) {
310
+ // Commit walk failed, fall back to main
311
+ const mainPath = this.resolveBranchPath(path, 'main');
312
+ return this.readObjectFromPath(mainPath);
313
+ }
314
+ }
315
+ // Last fallback: try main branch
316
+ const mainPath = this.resolveBranchPath(path, 'main');
317
+ return this.readObjectFromPath(mainPath);
318
+ }
319
+ /**
320
+ * Delete object from branch-specific path (COW layer)
321
+ * @protected - Available to subclasses for COW implementation
322
+ */
323
+ async deleteObjectFromBranch(path, branch) {
324
+ const branchPath = this.resolveBranchPath(path, branch);
325
+ return this.deleteObjectFromPath(branchPath);
326
+ }
327
+ /**
328
+ * List objects under path in branch (COW layer)
329
+ * @protected - Available to subclasses for COW implementation
330
+ */
331
+ async listObjectsInBranch(prefix, branch) {
332
+ const branchPrefix = this.resolveBranchPath(prefix, branch);
333
+ const paths = await this.listObjectsUnderPath(branchPrefix);
334
+ // Remove branch prefix from results
335
+ const targetBranch = branch || this.currentBranch || 'main';
336
+ const prefixToRemove = `branches/${targetBranch}/`;
337
+ return paths.map(p => p.startsWith(prefixToRemove) ? p.substring(prefixToRemove.length) : p);
338
+ }
339
+ /**
340
+ * List objects with inheritance (v5.0.1)
341
+ * Lists objects from current branch AND main branch, returns unique paths
342
+ * This enables fork to see parent's data in pagination operations
343
+ *
344
+ * Simplified approach: All branches inherit from main
345
+ */
346
+ async listObjectsWithInheritance(prefix, branch) {
347
+ if (!this.cowEnabled) {
348
+ return this.listObjectsInBranch(prefix, branch);
349
+ }
350
+ const targetBranch = branch || this.currentBranch || 'main';
351
+ // Collect paths from current branch
352
+ const pathsSet = new Set();
353
+ const currentBranchPaths = await this.listObjectsInBranch(prefix, targetBranch);
354
+ currentBranchPaths.forEach(p => pathsSet.add(p));
355
+ // If not on main, also list from main (all branches inherit from main)
356
+ if (targetBranch !== 'main') {
357
+ const mainPaths = await this.listObjectsInBranch(prefix, 'main');
358
+ mainPaths.forEach(p => pathsSet.add(p));
359
+ }
360
+ return Array.from(pathsSet);
361
+ }
230
362
  /**
231
363
  * Save a noun to storage (v4.0.0: vector only, metadata saved separately)
232
364
  * @param noun Pure HNSW vector data (no metadata)
@@ -839,7 +971,7 @@ export class BaseStorage extends BaseStorageAdapter {
839
971
  async saveMetadata(id, metadata) {
840
972
  await this.ensureInitialized();
841
973
  const keyInfo = this.analyzeKey(id, 'system');
842
- return this.writeObjectToPath(keyInfo.fullPath, metadata);
974
+ return this.writeObjectToBranch(keyInfo.fullPath, metadata);
843
975
  }
844
976
  /**
845
977
  * Get metadata from storage (v4.0.0: now typed)
@@ -848,7 +980,7 @@ export class BaseStorage extends BaseStorageAdapter {
848
980
  async getMetadata(id) {
849
981
  await this.ensureInitialized();
850
982
  const keyInfo = this.analyzeKey(id, 'system');
851
- return this.readObjectFromPath(keyInfo.fullPath);
983
+ return this.readWithInheritance(keyInfo.fullPath);
852
984
  }
853
985
  /**
854
986
  * Save noun metadata to storage (v4.0.0: now typed)
@@ -873,10 +1005,10 @@ export class BaseStorage extends BaseStorageAdapter {
873
1005
  await this.ensureInitialized();
874
1006
  // Determine if this is a new entity by checking if metadata already exists
875
1007
  const keyInfo = this.analyzeKey(id, 'noun-metadata');
876
- const existingMetadata = await this.readObjectFromPath(keyInfo.fullPath);
1008
+ const existingMetadata = await this.readWithInheritance(keyInfo.fullPath);
877
1009
  const isNew = !existingMetadata;
878
- // Save the metadata
879
- await this.writeObjectToPath(keyInfo.fullPath, metadata);
1010
+ // Save the metadata (COW-aware - writes to branch-specific path)
1011
+ await this.writeObjectToBranch(keyInfo.fullPath, metadata);
880
1012
  // CRITICAL FIX (v4.1.2): Increment count for new entities
881
1013
  // This runs AFTER metadata is saved, guaranteeing type information is available
882
1014
  // Uses synchronous increment since storage operations are already serialized
@@ -896,7 +1028,7 @@ export class BaseStorage extends BaseStorageAdapter {
896
1028
  async getNounMetadata(id) {
897
1029
  await this.ensureInitialized();
898
1030
  const keyInfo = this.analyzeKey(id, 'noun-metadata');
899
- return this.readObjectFromPath(keyInfo.fullPath);
1031
+ return this.readWithInheritance(keyInfo.fullPath);
900
1032
  }
901
1033
  /**
902
1034
  * Delete noun metadata from storage
@@ -905,7 +1037,7 @@ export class BaseStorage extends BaseStorageAdapter {
905
1037
  async deleteNounMetadata(id) {
906
1038
  await this.ensureInitialized();
907
1039
  const keyInfo = this.analyzeKey(id, 'noun-metadata');
908
- return this.deleteObjectFromPath(keyInfo.fullPath);
1040
+ return this.deleteObjectFromBranch(keyInfo.fullPath);
909
1041
  }
910
1042
  /**
911
1043
  * Save verb metadata to storage (v4.0.0: now typed)
@@ -931,10 +1063,10 @@ export class BaseStorage extends BaseStorageAdapter {
931
1063
  await this.ensureInitialized();
932
1064
  // Determine if this is a new verb by checking if metadata already exists
933
1065
  const keyInfo = this.analyzeKey(id, 'verb-metadata');
934
- const existingMetadata = await this.readObjectFromPath(keyInfo.fullPath);
1066
+ const existingMetadata = await this.readWithInheritance(keyInfo.fullPath);
935
1067
  const isNew = !existingMetadata;
936
- // Save the metadata
937
- await this.writeObjectToPath(keyInfo.fullPath, metadata);
1068
+ // Save the metadata (COW-aware - writes to branch-specific path)
1069
+ await this.writeObjectToBranch(keyInfo.fullPath, metadata);
938
1070
  // CRITICAL FIX (v4.1.2): Increment verb count for new relationships
939
1071
  // This runs AFTER metadata is saved
940
1072
  // Verb type is now stored in metadata (as of v4.1.2) to avoid loading HNSWVerb
@@ -955,7 +1087,7 @@ export class BaseStorage extends BaseStorageAdapter {
955
1087
  async getVerbMetadata(id) {
956
1088
  await this.ensureInitialized();
957
1089
  const keyInfo = this.analyzeKey(id, 'verb-metadata');
958
- return this.readObjectFromPath(keyInfo.fullPath);
1090
+ return this.readWithInheritance(keyInfo.fullPath);
959
1091
  }
960
1092
  /**
961
1093
  * Delete verb metadata from storage
@@ -964,7 +1096,7 @@ export class BaseStorage extends BaseStorageAdapter {
964
1096
  async deleteVerbMetadata(id) {
965
1097
  await this.ensureInitialized();
966
1098
  const keyInfo = this.analyzeKey(id, 'verb-metadata');
967
- return this.deleteObjectFromPath(keyInfo.fullPath);
1099
+ return this.deleteObjectFromBranch(keyInfo.fullPath);
968
1100
  }
969
1101
  /**
970
1102
  * Helper method to convert a Map to a plain object for serialization
@@ -49,6 +49,7 @@ export interface BlobWriteOptions {
49
49
  export interface BlobReadOptions {
50
50
  skipDecompression?: boolean;
51
51
  skipCache?: boolean;
52
+ skipVerification?: boolean;
52
53
  }
53
54
  /**
54
55
  * Blob statistics for observability
@@ -189,11 +189,13 @@ export class BlobStorage {
189
189
  finalData = await this.zstdDecompress(data);
190
190
  }
191
191
  // Verify hash (optional, expensive)
192
- if (!options.skipCache && BlobStorage.hash(finalData) !== hash) {
192
+ if (!options.skipVerification && BlobStorage.hash(finalData) !== hash) {
193
193
  throw new Error(`Blob integrity check failed: ${hash}`);
194
194
  }
195
- // Add to cache
196
- this.addToCache(hash, finalData, metadata);
195
+ // Add to cache (only if not skipped)
196
+ if (!options.skipCache) {
197
+ this.addToCache(hash, finalData, metadata);
198
+ }
197
199
  return finalData;
198
200
  }
199
201
  /**
@@ -292,10 +292,9 @@ export interface StorageOptions {
292
292
  };
293
293
  /**
294
294
  * COW (Copy-on-Write) configuration for instant fork() capability
295
- * v5.0.0+
295
+ * v5.0.1: COW is now always enabled (automatic, zero-config)
296
296
  */
297
297
  branch?: string;
298
- enableCOW?: boolean;
299
298
  enableCompression?: boolean;
300
299
  }
301
300
  /**
@@ -39,12 +39,13 @@ async function wrapWithTypeAware(underlying, options, verbose = false) {
39
39
  underlyingStorage: underlying,
40
40
  verbose
41
41
  });
42
- // Initialize COW if enabled
43
- if (options?.enableCOW && typeof wrapped.initializeCOW === 'function') {
44
- await wrapped.initializeCOW({
45
- branch: options.branch || 'main',
46
- enableCompression: options.enableCompression !== false
47
- });
42
+ // v5.0.1: COW will be initialized AFTER storage.init() in Brainy
43
+ // Store COW options for later initialization
44
+ if (typeof wrapped.initializeCOW === 'function') {
45
+ wrapped._cowOptions = {
46
+ branch: options?.branch || 'main',
47
+ enableCompression: options?.enableCompression !== false
48
+ };
48
49
  }
49
50
  return wrapped;
50
51
  }
@@ -459,7 +459,6 @@ export interface BrainyConfig {
459
459
  type: 'auto' | 'memory' | 'filesystem' | 's3' | 'r2' | 'opfs' | 'gcs';
460
460
  options?: any;
461
461
  branch?: string;
462
- enableCOW?: boolean;
463
462
  };
464
463
  model?: {
465
464
  type: 'fast' | 'accurate' | 'balanced' | 'custom';
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Usage:
8
8
  * import { FSCompat } from '@soulcraft/brainy/vfs'
9
- * const fs = new FSCompat(brain.vfs())
9
+ * const fs = new FSCompat(brain.vfs)
10
10
  *
11
11
  * // Now use like Node's fs
12
12
  * await fs.promises.readFile('/path')
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Usage:
8
8
  * import { FSCompat } from '@soulcraft/brainy/vfs'
9
- * const fs = new FSCompat(brain.vfs())
9
+ * const fs = new FSCompat(brain.vfs)
10
10
  *
11
11
  * // Now use like Node's fs
12
12
  * await fs.promises.readFile('/path')
@@ -42,10 +42,9 @@ export class VirtualFileSystem {
42
42
  return;
43
43
  // Merge config with defaults
44
44
  this.config = { ...this.getDefaultConfig(), ...config };
45
- // Initialize Brainy if needed
46
- if (!this.brain.isInitialized) {
47
- await this.brain.init();
48
- }
45
+ // v5.0.1: VFS is now auto-initialized during brain.init()
46
+ // Brain is guaranteed to be initialized when this is called
47
+ // Removed brain.init() check to prevent infinite recursion
49
48
  // Create or find root entity
50
49
  this.rootEntityId = await this.initializeRoot();
51
50
  // Initialize projection registry with auto-discovery of built-in projections
@@ -847,11 +846,11 @@ export class VirtualFileSystem {
847
846
  throw new VFSError(VFSErrorCode.EINVAL, 'VFS not initialized. Call await vfs.init() before using VFS operations.\n\n' +
848
847
  '✅ After brain.import():\n' +
849
848
  ' await brain.import(file, { vfsPath: "/imports/data" })\n' +
850
- ' const vfs = brain.vfs()\n' +
849
+ ' const vfs = brain.vfs\n' +
851
850
  ' await vfs.init() // ← Required! Safe to call multiple times\n' +
852
851
  ' const files = await vfs.readdir("/imports/data")\n\n' +
853
852
  '✅ Direct VFS usage:\n' +
854
- ' const vfs = brain.vfs()\n' +
853
+ ' const vfs = brain.vfs\n' +
855
854
  ' await vfs.init() // ← Always required before first use\n' +
856
855
  ' await vfs.writeFile("/docs/readme.md", "Hello")\n\n' +
857
856
  '📖 Docs: https://github.com/soulcraftlabs/brainy/blob/main/docs/vfs/QUICK_START.md', '<unknown>', 'VFS');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",