@soulcraft/brainy 5.8.0 → 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 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,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
- // 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({
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: '/', // ✅ Correct field name
110
- vfsType: 'directory' // ✅ Correct field name
138
+ path: '/',
139
+ vfsType: 'directory'
111
140
  },
112
- limit: 10
141
+ limit: 50, // Higher limit to catch all possible duplicates
142
+ excludeVFS: false // CRITICAL: Don't exclude VFS entities!
113
143
  });
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
- });
126
- }
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
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: rootEntity.id,
159
+ id: root.id,
134
160
  metadata: {
161
+ ...metadata,
135
162
  path: '/',
136
163
  name: '',
137
164
  vfsType: 'directory',
138
- isVFS: true, // v4.3.3: Mark as VFS entity (internal)
139
- isVFSEntity: true, // v5.3.0: Explicit flag for developer filtering
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 rootEntity.id;
176
+ return root.id;
151
177
  }
152
- // Create root directory (only if truly doesn't exist)
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: '/', // Root directory content as string
181
+ data: '/',
155
182
  type: NounType.Collection,
156
183
  metadata: {
157
184
  path: '/',
158
185
  name: '',
159
186
  vfsType: 'directory',
160
- isVFS: true, // v4.3.3: Mark as VFS entity (internal)
161
- isVFSEntity: true, // v5.3.0: Explicit flag for developer filtering
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() // Track creation time for duplicate detection
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
- // 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() });
204
- }
205
- // Apply encoding if requested
206
- if (options?.encoding) {
207
- return Buffer.from(content.toString(options.encoding));
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.8.0",
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/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"