@soulcraft/brainy 3.12.0 → 3.13.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.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * VFS Tree Utilities
3
+ * Provides safe tree operations that prevent common recursion issues
4
+ */
5
+ import { VFSEntity } from './types.js';
6
+ export interface TreeNode {
7
+ name: string;
8
+ path: string;
9
+ type: 'file' | 'directory';
10
+ entityId?: string;
11
+ children?: TreeNode[];
12
+ metadata?: any;
13
+ }
14
+ export interface TreeOptions {
15
+ maxDepth?: number;
16
+ includeHidden?: boolean;
17
+ filter?: (node: VFSEntity) => boolean;
18
+ sort?: 'name' | 'modified' | 'size';
19
+ expandAll?: boolean;
20
+ }
21
+ /**
22
+ * Tree utility functions for VFS
23
+ * These functions ensure proper tree structure without recursion issues
24
+ */
25
+ export declare class VFSTreeUtils {
26
+ /**
27
+ * Build a safe tree structure from VFS entities
28
+ * Guarantees no directory appears as its own child
29
+ */
30
+ static buildTree(entities: VFSEntity[], rootPath?: string, options?: TreeOptions): TreeNode;
31
+ /**
32
+ * Get direct children only - guaranteed no self-inclusion
33
+ */
34
+ static getDirectChildren(entities: VFSEntity[], parentPath: string): VFSEntity[];
35
+ /**
36
+ * Get all descendants (recursive children)
37
+ */
38
+ static getDescendants(entities: VFSEntity[], ancestorPath: string, includeAncestor?: boolean): VFSEntity[];
39
+ /**
40
+ * Flatten a tree structure back to a list
41
+ */
42
+ static flattenTree(node: TreeNode): TreeNode[];
43
+ /**
44
+ * Find a node in the tree by path
45
+ */
46
+ static findNode(root: TreeNode, targetPath: string): TreeNode | null;
47
+ /**
48
+ * Calculate tree statistics
49
+ */
50
+ static getTreeStats(node: TreeNode): {
51
+ totalNodes: number;
52
+ files: number;
53
+ directories: number;
54
+ maxDepth: number;
55
+ totalSize?: number;
56
+ };
57
+ private static getParentPath;
58
+ private static sortTreeNodes;
59
+ private static limitDepth;
60
+ /**
61
+ * Validate tree structure - ensures no recursion
62
+ */
63
+ static validateTree(node: TreeNode, visited?: Set<string>): {
64
+ valid: boolean;
65
+ errors: string[];
66
+ };
67
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * VFS Tree Utilities
3
+ * Provides safe tree operations that prevent common recursion issues
4
+ */
5
+ /**
6
+ * Tree utility functions for VFS
7
+ * These functions ensure proper tree structure without recursion issues
8
+ */
9
+ export class VFSTreeUtils {
10
+ /**
11
+ * Build a safe tree structure from VFS entities
12
+ * Guarantees no directory appears as its own child
13
+ */
14
+ static buildTree(entities, rootPath = '/', options = {}) {
15
+ const pathToEntity = new Map();
16
+ const pathToNode = new Map();
17
+ // First pass: index all entities by path
18
+ for (const entity of entities) {
19
+ const path = entity.metadata.path;
20
+ // Critical: Skip if entity IS the root we're building from
21
+ if (path === rootPath) {
22
+ continue;
23
+ }
24
+ pathToEntity.set(path, entity);
25
+ }
26
+ // Create root node
27
+ const rootNode = {
28
+ name: rootPath === '/' ? 'root' : rootPath.split('/').pop(),
29
+ path: rootPath,
30
+ type: 'directory',
31
+ children: []
32
+ };
33
+ pathToNode.set(rootPath, rootNode);
34
+ // Second pass: build tree structure
35
+ const sortedPaths = Array.from(pathToEntity.keys()).sort();
36
+ for (const path of sortedPaths) {
37
+ const entity = pathToEntity.get(path);
38
+ // Apply filter if provided
39
+ if (options.filter && !options.filter(entity)) {
40
+ continue;
41
+ }
42
+ // Skip hidden files if requested
43
+ if (!options.includeHidden && entity.metadata.name.startsWith('.')) {
44
+ continue;
45
+ }
46
+ // Create node for this entity
47
+ const node = {
48
+ name: entity.metadata.name,
49
+ path: entity.metadata.path,
50
+ type: entity.metadata.vfsType === 'directory' ? 'directory' : 'file',
51
+ entityId: entity.id,
52
+ metadata: entity.metadata
53
+ };
54
+ if (entity.metadata.vfsType === 'directory') {
55
+ node.children = [];
56
+ }
57
+ pathToNode.set(path, node);
58
+ // Find parent and attach
59
+ const parentPath = this.getParentPath(path);
60
+ const parentNode = pathToNode.get(parentPath);
61
+ if (parentNode && parentNode.children) {
62
+ parentNode.children.push(node);
63
+ }
64
+ }
65
+ // Sort children if requested
66
+ if (options.sort) {
67
+ this.sortTreeNodes(rootNode, options.sort);
68
+ }
69
+ // Apply depth limit if specified
70
+ if (options.maxDepth !== undefined) {
71
+ this.limitDepth(rootNode, options.maxDepth);
72
+ }
73
+ return rootNode;
74
+ }
75
+ /**
76
+ * Get direct children only - guaranteed no self-inclusion
77
+ */
78
+ static getDirectChildren(entities, parentPath) {
79
+ const children = [];
80
+ const parentDepth = parentPath === '/' ? 0 : parentPath.split('/').length - 1;
81
+ for (const entity of entities) {
82
+ const path = entity.metadata.path;
83
+ // Critical check 1: Skip if this IS the parent
84
+ if (path === parentPath) {
85
+ continue;
86
+ }
87
+ // Check if entity is a direct child
88
+ if (path.startsWith(parentPath)) {
89
+ const relativePath = parentPath === '/'
90
+ ? path.substring(1)
91
+ : path.substring(parentPath.length + 1);
92
+ // Direct child has no additional slashes
93
+ if (!relativePath.includes('/')) {
94
+ children.push(entity);
95
+ }
96
+ }
97
+ }
98
+ return children;
99
+ }
100
+ /**
101
+ * Get all descendants (recursive children)
102
+ */
103
+ static getDescendants(entities, ancestorPath, includeAncestor = false) {
104
+ const descendants = [];
105
+ for (const entity of entities) {
106
+ const path = entity.metadata.path;
107
+ // Include ancestor only if explicitly requested
108
+ if (path === ancestorPath) {
109
+ if (includeAncestor) {
110
+ descendants.push(entity);
111
+ }
112
+ continue;
113
+ }
114
+ // Check if entity is under ancestor path
115
+ const prefix = ancestorPath === '/' ? '/' : ancestorPath + '/';
116
+ if (path.startsWith(prefix)) {
117
+ descendants.push(entity);
118
+ }
119
+ }
120
+ return descendants;
121
+ }
122
+ /**
123
+ * Flatten a tree structure back to a list
124
+ */
125
+ static flattenTree(node) {
126
+ const result = [node];
127
+ if (node.children) {
128
+ for (const child of node.children) {
129
+ result.push(...this.flattenTree(child));
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+ /**
135
+ * Find a node in the tree by path
136
+ */
137
+ static findNode(root, targetPath) {
138
+ if (root.path === targetPath) {
139
+ return root;
140
+ }
141
+ if (root.children) {
142
+ for (const child of root.children) {
143
+ const found = this.findNode(child, targetPath);
144
+ if (found)
145
+ return found;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ /**
151
+ * Calculate tree statistics
152
+ */
153
+ static getTreeStats(node) {
154
+ let stats = {
155
+ totalNodes: 0,
156
+ files: 0,
157
+ directories: 0,
158
+ maxDepth: 0,
159
+ totalSize: 0
160
+ };
161
+ function traverse(n, depth) {
162
+ stats.totalNodes++;
163
+ stats.maxDepth = Math.max(stats.maxDepth, depth);
164
+ if (n.type === 'file') {
165
+ stats.files++;
166
+ if (n.metadata?.size) {
167
+ stats.totalSize += n.metadata.size;
168
+ }
169
+ }
170
+ else {
171
+ stats.directories++;
172
+ }
173
+ if (n.children) {
174
+ for (const child of n.children) {
175
+ traverse(child, depth + 1);
176
+ }
177
+ }
178
+ }
179
+ traverse(node, 0);
180
+ return stats;
181
+ }
182
+ // Helper methods
183
+ static getParentPath(path) {
184
+ if (path === '/')
185
+ return '/';
186
+ const parts = path.split('/');
187
+ parts.pop();
188
+ return parts.length === 1 ? '/' : parts.join('/');
189
+ }
190
+ static sortTreeNodes(node, sortBy) {
191
+ if (!node.children)
192
+ return;
193
+ node.children.sort((a, b) => {
194
+ // Directories first, then files
195
+ if (a.type !== b.type) {
196
+ return a.type === 'directory' ? -1 : 1;
197
+ }
198
+ switch (sortBy) {
199
+ case 'name':
200
+ return a.name.localeCompare(b.name);
201
+ case 'modified':
202
+ const aTime = a.metadata?.modified || 0;
203
+ const bTime = b.metadata?.modified || 0;
204
+ return bTime - aTime;
205
+ case 'size':
206
+ const aSize = a.metadata?.size || 0;
207
+ const bSize = b.metadata?.size || 0;
208
+ return bSize - aSize;
209
+ default:
210
+ return 0;
211
+ }
212
+ });
213
+ // Recursively sort children
214
+ for (const child of node.children) {
215
+ this.sortTreeNodes(child, sortBy);
216
+ }
217
+ }
218
+ static limitDepth(node, maxDepth, currentDepth = 0) {
219
+ if (currentDepth >= maxDepth) {
220
+ delete node.children;
221
+ return;
222
+ }
223
+ if (node.children) {
224
+ for (const child of node.children) {
225
+ this.limitDepth(child, maxDepth, currentDepth + 1);
226
+ }
227
+ }
228
+ }
229
+ /**
230
+ * Validate tree structure - ensures no recursion
231
+ */
232
+ static validateTree(node, visited = new Set()) {
233
+ const errors = [];
234
+ // Check for cycles
235
+ if (visited.has(node.path)) {
236
+ errors.push(`Cycle detected at path: ${node.path}`);
237
+ return { valid: false, errors };
238
+ }
239
+ visited.add(node.path);
240
+ // Check children
241
+ if (node.children) {
242
+ const childPaths = new Set();
243
+ for (const child of node.children) {
244
+ // Check for duplicate children
245
+ if (childPaths.has(child.path)) {
246
+ errors.push(`Duplicate child path: ${child.path}`);
247
+ }
248
+ childPaths.add(child.path);
249
+ // Check child is not parent
250
+ if (child.path === node.path) {
251
+ errors.push(`Directory contains itself: ${node.path}`);
252
+ }
253
+ // Check child is actually under parent
254
+ if (node.path !== '/' && !child.path.startsWith(node.path + '/')) {
255
+ errors.push(`Child ${child.path} not under parent ${node.path}`);
256
+ }
257
+ // Recursively validate children
258
+ const childValidation = this.validateTree(child, new Set(visited));
259
+ errors.push(...childValidation.errors);
260
+ }
261
+ }
262
+ return {
263
+ valid: errors.length === 0,
264
+ errors
265
+ };
266
+ }
267
+ }
268
+ //# sourceMappingURL=TreeUtils.js.map
@@ -52,6 +52,37 @@ export declare class VirtualFileSystem implements IVirtualFileSystem {
52
52
  * Delete a file
53
53
  */
54
54
  unlink(path: string): Promise<void>;
55
+ /**
56
+ * Get only direct children of a directory - guaranteed no self-inclusion
57
+ * This is the SAFE way to get children for building tree UIs
58
+ */
59
+ getDirectChildren(path: string): Promise<VFSEntity[]>;
60
+ /**
61
+ * Get a properly structured tree for the given path
62
+ * This prevents recursion issues common when building file explorers
63
+ */
64
+ getTreeStructure(path: string, options?: {
65
+ maxDepth?: number;
66
+ includeHidden?: boolean;
67
+ sort?: 'name' | 'modified' | 'size';
68
+ }): Promise<any>;
69
+ /**
70
+ * Get all descendants of a directory (flat list)
71
+ */
72
+ getDescendants(path: string, options?: {
73
+ includeAncestor?: boolean;
74
+ type?: 'file' | 'directory';
75
+ }): Promise<VFSEntity[]>;
76
+ /**
77
+ * Inspect a path and return structured information
78
+ * This is the recommended method for file explorers to use
79
+ */
80
+ inspect(path: string): Promise<{
81
+ node: VFSEntity;
82
+ children: VFSEntity[];
83
+ parent: VFSEntity | null;
84
+ stats: VFSStats;
85
+ }>;
55
86
  /**
56
87
  * Create a directory
57
88
  */
@@ -322,6 +322,116 @@ export class VirtualFileSystem {
322
322
  this.triggerWatchers(path, 'rename');
323
323
  // Knowledge Layer hooks will be added by augmentation if enabled
324
324
  }
325
+ // ============= Tree Operations (NEW) =============
326
+ /**
327
+ * Get only direct children of a directory - guaranteed no self-inclusion
328
+ * This is the SAFE way to get children for building tree UIs
329
+ */
330
+ async getDirectChildren(path) {
331
+ await this.ensureInitialized();
332
+ const entityId = await this.pathResolver.resolve(path);
333
+ const entity = await this.getEntityById(entityId);
334
+ // Verify it's a directory
335
+ if (entity.metadata.vfsType !== 'directory') {
336
+ throw new VFSError(VFSErrorCode.ENOTDIR, `Not a directory: ${path}`, path, 'getDirectChildren');
337
+ }
338
+ // Use the safe getChildren from PathResolver
339
+ const children = await this.pathResolver.getChildren(entityId);
340
+ // Double-check no self-inclusion (paranoid safety)
341
+ return children.filter(child => child.metadata.path !== path);
342
+ }
343
+ /**
344
+ * Get a properly structured tree for the given path
345
+ * This prevents recursion issues common when building file explorers
346
+ */
347
+ async getTreeStructure(path, options) {
348
+ await this.ensureInitialized();
349
+ const { VFSTreeUtils } = await import('./TreeUtils.js');
350
+ const entityId = await this.pathResolver.resolve(path);
351
+ const entity = await this.getEntityById(entityId);
352
+ if (entity.metadata.vfsType !== 'directory') {
353
+ throw new VFSError(VFSErrorCode.ENOTDIR, `Not a directory: ${path}`, path, 'getTreeStructure');
354
+ }
355
+ // Recursively gather all descendants
356
+ const allEntities = [];
357
+ const visited = new Set();
358
+ const gatherDescendants = async (dirId) => {
359
+ if (visited.has(dirId))
360
+ return; // Prevent cycles
361
+ visited.add(dirId);
362
+ const children = await this.pathResolver.getChildren(dirId);
363
+ for (const child of children) {
364
+ allEntities.push(child);
365
+ if (child.metadata.vfsType === 'directory') {
366
+ await gatherDescendants(child.id);
367
+ }
368
+ }
369
+ };
370
+ await gatherDescendants(entityId);
371
+ // Build safe tree structure
372
+ return VFSTreeUtils.buildTree(allEntities, path, options || {});
373
+ }
374
+ /**
375
+ * Get all descendants of a directory (flat list)
376
+ */
377
+ async getDescendants(path, options) {
378
+ await this.ensureInitialized();
379
+ const entityId = await this.pathResolver.resolve(path);
380
+ const entity = await this.getEntityById(entityId);
381
+ if (entity.metadata.vfsType !== 'directory') {
382
+ throw new VFSError(VFSErrorCode.ENOTDIR, `Not a directory: ${path}`, path, 'getDescendants');
383
+ }
384
+ const descendants = [];
385
+ if (options?.includeAncestor) {
386
+ descendants.push(entity);
387
+ }
388
+ const visited = new Set();
389
+ const queue = [entityId];
390
+ while (queue.length > 0) {
391
+ const currentId = queue.shift();
392
+ if (visited.has(currentId))
393
+ continue;
394
+ visited.add(currentId);
395
+ const children = await this.pathResolver.getChildren(currentId);
396
+ for (const child of children) {
397
+ // Filter by type if specified
398
+ if (!options?.type || child.metadata.vfsType === options.type) {
399
+ descendants.push(child);
400
+ }
401
+ // Add directories to queue for traversal
402
+ if (child.metadata.vfsType === 'directory') {
403
+ queue.push(child.id);
404
+ }
405
+ }
406
+ }
407
+ return descendants;
408
+ }
409
+ /**
410
+ * Inspect a path and return structured information
411
+ * This is the recommended method for file explorers to use
412
+ */
413
+ async inspect(path) {
414
+ await this.ensureInitialized();
415
+ const entityId = await this.pathResolver.resolve(path);
416
+ const entity = await this.getEntityById(entityId);
417
+ const stats = await this.stat(path);
418
+ let children = [];
419
+ if (entity.metadata.vfsType === 'directory') {
420
+ children = await this.getDirectChildren(path);
421
+ }
422
+ let parent = null;
423
+ if (path !== '/') {
424
+ const parentPath = path.substring(0, path.lastIndexOf('/')) || '/';
425
+ const parentId = await this.pathResolver.resolve(parentPath);
426
+ parent = await this.getEntityById(parentId);
427
+ }
428
+ return {
429
+ node: entity,
430
+ children,
431
+ parent,
432
+ stats
433
+ };
434
+ }
325
435
  // ============= Directory Operations =============
326
436
  /**
327
437
  * Create a directory
@@ -283,6 +283,22 @@ export interface IVirtualFileSystem {
283
283
  recursive?: boolean;
284
284
  }): Promise<void>;
285
285
  readdir(path: string, options?: ReaddirOptions): Promise<string[] | VFSDirent[]>;
286
+ getDirectChildren(path: string): Promise<VFSEntity[]>;
287
+ getTreeStructure(path: string, options?: {
288
+ maxDepth?: number;
289
+ includeHidden?: boolean;
290
+ sort?: 'name' | 'modified' | 'size';
291
+ }): Promise<any>;
292
+ getDescendants(path: string, options?: {
293
+ includeAncestor?: boolean;
294
+ type?: 'file' | 'directory';
295
+ }): Promise<VFSEntity[]>;
296
+ inspect(path: string): Promise<{
297
+ node: VFSEntity;
298
+ children: VFSEntity[];
299
+ parent: VFSEntity | null;
300
+ stats: VFSStats;
301
+ }>;
286
302
  stat(path: string): Promise<VFSStats>;
287
303
  lstat(path: string): Promise<VFSStats>;
288
304
  exists(path: string): Promise<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "3.12.0",
3
+ "version": "3.13.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",