@soulcraft/brainy 5.8.0 → 5.10.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 CHANGED
@@ -2,6 +2,11 @@
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.9.0](https://github.com/soulcraftlabs/brainy/compare/v5.8.0...v5.9.0) (2025-11-14)
6
+
7
+ - fix: resolve VFS tree corruption from blob errors (v5.8.0) (93d2d70)
8
+
9
+
5
10
  ### [5.8.0](https://github.com/soulcraftlabs/brainy/compare/v5.7.13...v5.8.0) (2025-11-14)
6
11
 
7
12
  - feat: add v5.8.0 features - transactions, pagination, and comprehensive docs (e40fee3)
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Image Import Handler (v5.2.0)
2
+ * Image Import Handler (v5.8.0 - Pure JavaScript)
3
3
  *
4
4
  * Handles image files with:
5
- * - EXIF metadata extraction (camera, GPS, timestamps)
6
- * - Thumbnail generation (multiple sizes)
7
- * - Image metadata (dimensions, format, color space)
8
- * - Support for JPEG, PNG, WebP, GIF, TIFF, AVIF, etc.
5
+ * - EXIF metadata extraction (camera, GPS, timestamps) via exifr
6
+ * - Image metadata (dimensions, format) via probe-image-size
7
+ * - Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG
9
8
  *
10
- * NO MOCKS - Production implementation using sharp and exifr
9
+ * NO NATIVE DEPENDENCIES - Pure JavaScript implementation
10
+ * Replaces Sharp (v5.7.x) with lightweight pure-JS alternatives
11
11
  */
12
12
  import { BaseFormatHandler } from './base.js';
13
13
  import type { FormatHandlerOptions, ProcessedData } from '../types.js';
@@ -17,18 +17,12 @@ export interface ImageMetadata {
17
17
  height: number;
18
18
  /** Image format (jpeg, png, webp, etc.) */
19
19
  format: string;
20
- /** Color space */
21
- space: string;
22
- /** Number of channels */
23
- channels: number;
24
- /** Bit depth */
25
- depth: string;
26
20
  /** File size in bytes */
27
21
  size: number;
28
- /** Whether image has alpha channel */
29
- hasAlpha: boolean;
30
22
  /** Orientation (EXIF) */
31
23
  orientation?: number;
24
+ /** MIME type */
25
+ mimeType?: string;
32
26
  }
33
27
  export interface EXIFData {
34
28
  /** Camera make (e.g., "Canon", "Nikon") */
@@ -74,6 +68,8 @@ export interface ImageHandlerOptions extends FormatHandlerOptions {
74
68
  * Processes image files and extracts rich metadata including EXIF data.
75
69
  * Enables developers to import images into the knowledge graph with
76
70
  * full metadata extraction.
71
+ *
72
+ * v5.8.0: Pure JavaScript implementation (no native dependencies)
77
73
  */
78
74
  export declare class ImageHandler extends BaseFormatHandler {
79
75
  readonly format = "image";
@@ -89,11 +85,11 @@ export declare class ImageHandler extends BaseFormatHandler {
89
85
  */
90
86
  process(data: Buffer | string, options?: ImageHandlerOptions): Promise<ProcessedData>;
91
87
  /**
92
- * Extract image metadata using sharp
88
+ * Extract image metadata using probe-image-size (pure JS)
93
89
  */
94
90
  private extractMetadata;
95
91
  /**
96
- * Extract EXIF data using exifr
92
+ * Extract EXIF data using exifr (pure JS)
97
93
  */
98
94
  private extractEXIF;
99
95
  /**
@@ -1,23 +1,26 @@
1
1
  /**
2
- * Image Import Handler (v5.2.0)
2
+ * Image Import Handler (v5.8.0 - Pure JavaScript)
3
3
  *
4
4
  * Handles image files with:
5
- * - EXIF metadata extraction (camera, GPS, timestamps)
6
- * - Thumbnail generation (multiple sizes)
7
- * - Image metadata (dimensions, format, color space)
8
- * - Support for JPEG, PNG, WebP, GIF, TIFF, AVIF, etc.
5
+ * - EXIF metadata extraction (camera, GPS, timestamps) via exifr
6
+ * - Image metadata (dimensions, format) via probe-image-size
7
+ * - Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG
9
8
  *
10
- * NO MOCKS - Production implementation using sharp and exifr
9
+ * NO NATIVE DEPENDENCIES - Pure JavaScript implementation
10
+ * Replaces Sharp (v5.7.x) with lightweight pure-JS alternatives
11
11
  */
12
12
  import { BaseFormatHandler } from './base.js';
13
- import sharp from 'sharp';
14
13
  import exifr from 'exifr';
14
+ import probeImageSize from 'probe-image-size';
15
+ import { Readable } from 'stream';
15
16
  /**
16
17
  * ImageImportHandler
17
18
  *
18
19
  * Processes image files and extracts rich metadata including EXIF data.
19
20
  * Enables developers to import images into the knowledge graph with
20
21
  * full metadata extraction.
22
+ *
23
+ * v5.8.0: Pure JavaScript implementation (no native dependencies)
21
24
  */
22
25
  export class ImageHandler extends BaseFormatHandler {
23
26
  constructor() {
@@ -93,25 +96,36 @@ export class ImageHandler extends BaseFormatHandler {
93
96
  }
94
97
  }
95
98
  /**
96
- * Extract image metadata using sharp
99
+ * Extract image metadata using probe-image-size (pure JS)
97
100
  */
98
101
  async extractMetadata(buffer) {
99
- const image = sharp(buffer);
100
- const metadata = await image.metadata();
101
- return {
102
- width: metadata.width || 0,
103
- height: metadata.height || 0,
104
- format: metadata.format || 'unknown',
105
- space: metadata.space || 'unknown',
106
- channels: metadata.channels || 0,
107
- depth: metadata.depth || 'unknown',
108
- size: buffer.length,
109
- hasAlpha: metadata.hasAlpha || false,
110
- orientation: metadata.orientation
111
- };
102
+ try {
103
+ // Convert Buffer to Stream for probe-image-size
104
+ const stream = Readable.from(buffer);
105
+ const result = await probeImageSize(stream);
106
+ return {
107
+ width: result.width,
108
+ height: result.height,
109
+ format: result.type, // 'jpeg', 'png', 'webp', etc.
110
+ size: buffer.length,
111
+ mimeType: result.mime,
112
+ orientation: result.orientation
113
+ };
114
+ }
115
+ catch (error) {
116
+ // Fallback: Try to detect format from magic bytes
117
+ const detectedFormat = this.detectImageFormat(buffer);
118
+ return {
119
+ width: 0,
120
+ height: 0,
121
+ format: detectedFormat || 'unknown',
122
+ size: buffer.length,
123
+ mimeType: detectedFormat ? `image/${detectedFormat}` : undefined
124
+ };
125
+ }
112
126
  }
113
127
  /**
114
- * Extract EXIF data using exifr
128
+ * Extract EXIF data using exifr (pure JS)
115
129
  */
116
130
  async extractEXIF(buffer) {
117
131
  try {
@@ -199,6 +213,10 @@ export class ImageHandler extends BaseFormatHandler {
199
213
  (buffer[0] === 0x4d && buffer[1] === 0x4d && buffer[2] === 0x00 && buffer[3] === 0x2a)) {
200
214
  return 'tiff';
201
215
  }
216
+ // BMP: 42 4D
217
+ if (buffer[0] === 0x42 && buffer[1] === 0x4d) {
218
+ return 'bmp';
219
+ }
202
220
  return null;
203
221
  }
204
222
  /**
@@ -29,6 +29,8 @@ export declare class VirtualFileSystem implements IVirtualFileSystem {
29
29
  private watchers;
30
30
  private backgroundTimer;
31
31
  private mkdirLocks;
32
+ private rootInitPromise;
33
+ private static readonly VFS_ROOT_ID;
32
34
  constructor(brain?: Brainy);
33
35
  /**
34
36
  * v5.2.0: Access to BlobStorage for unified file storage
@@ -46,7 +48,32 @@ export declare class VirtualFileSystem implements IVirtualFileSystem {
46
48
  * Zero-config: All semantic dimensions work out of the box
47
49
  */
48
50
  private registerBuiltInProjections;
51
+ /**
52
+ * v5.8.0: CRITICAL FIX - Prevent duplicate root creation
53
+ * Uses singleton promise pattern to ensure only ONE root initialization
54
+ * happens even with concurrent init() calls
55
+ */
49
56
  private initializeRoot;
57
+ /**
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.
63
+ */
64
+ private doInitializeRoot;
65
+ /**
66
+ * v5.10.0: Get standard root metadata
67
+ * Centralized to ensure consistency
68
+ */
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;
50
77
  /**
51
78
  * Read a file's content
52
79
  */
@@ -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();
@@ -59,6 +61,8 @@ export class VirtualFileSystem {
59
61
  // Removed brain.init() check to prevent infinite recursion
60
62
  // Create or find root entity
61
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();
62
66
  // Initialize projection registry with auto-discovery of built-in projections
63
67
  this.projectionRegistry = new ProjectionRegistry();
64
68
  this.registerBuiltInProjections();
@@ -99,76 +103,140 @@ export class VirtualFileSystem {
99
103
  }
100
104
  }
101
105
  }
106
+ /**
107
+ * v5.8.0: CRITICAL FIX - Prevent duplicate root creation
108
+ * Uses singleton promise pattern to ensure only ONE root initialization
109
+ * happens even with concurrent init() calls
110
+ */
102
111
  async initializeRoot() {
103
- // FIXED (v4.3.3): Use correct field names in where clause
104
- // Metadata index stores flat fields: path, vfsType, name
105
- // NOT nested: 'metadata.path', 'metadata.vfsType'
106
- const existing = await this.brain.find({
107
- type: NounType.Collection,
108
- where: {
109
- path: '/', // ✅ Correct field name
110
- vfsType: 'directory' // Correct field name
111
- },
112
- limit: 10
113
- });
114
- if (existing.length > 0) {
115
- // Handle duplicate roots (Workshop team reported ~10 duplicates!)
116
- if (existing.length > 1) {
117
- console.warn(`⚠️ Found ${existing.length} root entities! Using first one, consider cleanup.`);
118
- // Sort by creation time - use oldest root (most likely to have children)
119
- // v4.5.3: FIX - createdAt is in entity object, not at Result level!
120
- // brain.find() returns Result[], which has entity.createdAt, not top-level createdAt
121
- existing.sort((a, b) => {
122
- const aTime = a.entity?.createdAt || a.metadata?.modified || 0;
123
- const bTime = b.entity?.createdAt || b.metadata?.modified || 0;
124
- return aTime - bTime;
125
- });
112
+ // If initialization already in progress, wait for it (automatic mutex)
113
+ if (this.rootInitPromise) {
114
+ return await this.rootInitPromise;
115
+ }
116
+ // Start initialization and cache the promise
117
+ this.rootInitPromise = this.doInitializeRoot();
118
+ try {
119
+ const rootId = await this.rootInitPromise;
120
+ return rootId;
121
+ }
122
+ catch (error) {
123
+ // On error, clear promise so retry is possible
124
+ this.rootInitPromise = null;
125
+ throw error;
126
+ }
127
+ // NOTE: On success, we intentionally keep the promise cached
128
+ // This prevents re-initialization and serves as a cache
129
+ }
130
+ /**
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.
136
+ */
137
+ async doInitializeRoot() {
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;
126
153
  }
127
- const rootEntity = existing[0];
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
132
- await this.brain.update({
133
- id: rootEntity.id,
134
- metadata: {
135
- path: '/',
136
- name: '',
137
- vfsType: 'directory',
138
- isVFS: true, // v4.3.3: Mark as VFS entity (internal)
139
- isVFSEntity: true, // v5.3.0: Explicit flag for developer filtering
140
- size: 0,
141
- permissions: 0o755,
142
- owner: 'root',
143
- group: 'root',
144
- accessed: Date.now(),
145
- modified: Date.now(),
146
- ...entityMetadata // Preserve any existing metadata
147
- }
148
- });
154
+ }
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;
149
178
  }
150
- return rootEntity.id;
179
+ // Unexpected error
180
+ throw error;
151
181
  }
152
- // Create root directory (only if truly doesn't exist)
153
- const root = await this.brain.add({
154
- data: '/', // Root directory content as string
155
- type: NounType.Collection,
156
- metadata: {
157
- path: '/',
158
- name: '',
159
- vfsType: 'directory',
160
- isVFS: true, // v4.3.3: Mark as VFS entity (internal)
161
- isVFSEntity: true, // v5.3.0: Explicit flag for developer filtering
162
- size: 0,
163
- permissions: 0o755,
164
- owner: 'root',
165
- group: 'root',
166
- accessed: Date.now(),
167
- modified: Date.now(),
168
- createdAt: Date.now() // Track creation time for duplicate detection
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
+ };
201
+ }
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.
207
+ */
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
219
+ });
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');
169
234
  }
170
- });
171
- return root;
235
+ }
236
+ catch (error) {
237
+ // Non-critical error - log and continue
238
+ console.warn('VFS: Cleanup of old roots failed (non-critical):', error);
239
+ }
172
240
  }
173
241
  // ============= File Operations =============
174
242
  /**
@@ -194,19 +262,30 @@ export class VirtualFileSystem {
194
262
  if (!entity.metadata.storage?.type || entity.metadata.storage.type !== 'blob') {
195
263
  throw new VFSError(VFSErrorCode.EIO, `File has no blob storage: ${path}. Requires v5.2.0+ storage format.`, path, 'readFile');
196
264
  }
197
- // Read from BlobStorage (handles decompression automatically)
198
- const content = await this.blobStorage.read(entity.metadata.storage.hash);
199
- // Update access time
200
- await this.updateAccessTime(entityId);
201
- // Cache the content
202
- if (options?.cache !== false) {
203
- this.contentCache.set(path, { data: content, timestamp: Date.now() });
265
+ // v5.8.0: CRITICAL FIX - Isolate blob errors from VFS tree corruption
266
+ // Blob read errors MUST NOT cascade to VFS tree structure
267
+ try {
268
+ // Read from BlobStorage (handles decompression automatically)
269
+ const content = await this.blobStorage.read(entity.metadata.storage.hash);
270
+ // Update access time
271
+ await this.updateAccessTime(entityId);
272
+ // Cache the content
273
+ if (options?.cache !== false) {
274
+ this.contentCache.set(path, { data: content, timestamp: Date.now() });
275
+ }
276
+ // Apply encoding if requested
277
+ if (options?.encoding) {
278
+ return Buffer.from(content.toString(options.encoding));
279
+ }
280
+ return content;
204
281
  }
205
- // Apply encoding if requested
206
- if (options?.encoding) {
207
- return Buffer.from(content.toString(options.encoding));
282
+ catch (blobError) {
283
+ // Blob error isolated - VFS tree structure remains intact
284
+ const errorMsg = blobError instanceof Error ? blobError.message : String(blobError);
285
+ console.error(`VFS: Cannot read blob for ${path}:`, errorMsg);
286
+ // Throw VFSError (not blob error) - prevents cascading corruption
287
+ throw new VFSError(VFSErrorCode.EIO, `File read failed: ${errorMsg}`, path, 'readFile');
208
288
  }
209
- return content;
210
289
  }
211
290
  /**
212
291
  * Write a file
@@ -2050,4 +2129,7 @@ export class VirtualFileSystem {
2050
2129
  return await this.pathResolver.resolve(normalizedPath);
2051
2130
  }
2052
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';
2053
2135
  //# sourceMappingURL=VirtualFileSystem.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "5.8.0",
3
+ "version": "5.10.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/sharp": "^0.31.1",
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"