diff --git a/src/vfs/PathResolver.ts b/src/vfs/PathResolver.ts index d027dd32..30277a71 100644 --- a/src/vfs/PathResolver.ts +++ b/src/vfs/PathResolver.ts @@ -185,47 +185,35 @@ export class PathResolver { /** * Resolve a child entity by name within a parent directory + * Uses proper graph relationships instead of metadata queries */ private async resolveChild(parentId: string, name: string): Promise { // Check parent cache first const cachedChildren = this.parentCache.get(parentId) if (cachedChildren && cachedChildren.has(name)) { - // We know this child exists, find it by path since parent queries don't work - const parentEntity = await this.getEntity(parentId) - const parentPath = parentEntity.metadata.path - const childPath = this.joinPath(parentPath, name) - - const pathResults = await this.brain.find({ - where: { path: childPath }, - limit: 1 - }) - - if (pathResults.length > 0) { - return pathResults[0].entity.id - } + // Use cached knowledge to quickly find the child + // Still need to verify it exists } - // Since parent field queries don't work reliably, construct the expected path - // and query by path instead - const parentEntity = await this.getEntity(parentId) - const parentPath = parentEntity.metadata.path - const childPath = this.joinPath(parentPath, name) - - const results = await this.brain.find({ - where: { path: childPath }, - limit: 1 + // Use proper graph traversal to find children + // Get all relationships where parentId contains other entities + const relations = await this.brain.getRelations({ + from: parentId, + type: VerbType.Contains }) - if (results.length > 0) { - const childId = results[0].entity.id + // Find the child with matching name + for (const relation of relations) { + const childEntity = await this.brain.get(relation.to) + if (childEntity && childEntity.metadata?.name === name) { + // Update parent cache + if (!this.parentCache.has(parentId)) { + this.parentCache.set(parentId, new Set()) + } + this.parentCache.get(parentId)!.add(name) - // Update parent cache - if (!this.parentCache.has(parentId)) { - this.parentCache.set(parentId, new Set()) + return childEntity.id } - this.parentCache.get(parentId)!.add(name) - - return childId } return null @@ -233,33 +221,28 @@ export class PathResolver { /** * Get all children of a directory + * Uses proper graph relationships to traverse the tree */ async getChildren(dirId: string): Promise { - // Use Brainy's relationship query to find all children - const results = await this.brain.find({ - connected: { - from: dirId, - via: VerbType.Contains - }, - limit: 10000 // Large limit for directories + // Use proper graph API to get all Contains relationships from this directory + const relations = await this.brain.getRelations({ + from: dirId, + type: VerbType.Contains }) - // Filter and process valid VFS entities only const validChildren: VFSEntity[] = [] const childNames = new Set() - for (const result of results) { - const entity = result.entity - // Only include entities with proper VFS metadata and non-empty names - if (entity.metadata?.vfsType && - entity.metadata?.name && - entity.metadata?.path && - entity.id !== dirId) { // Don't include the directory itself + // Fetch all child entities + for (const relation of relations) { + const entity = await this.brain.get(relation.to) + if (entity && entity.metadata?.vfsType && entity.metadata?.name) { validChildren.push(entity as VFSEntity) childNames.add(entity.metadata.name) } } + // Update cache this.parentCache.set(dirId, childNames) return validChildren } diff --git a/src/vfs/VirtualFileSystem.ts b/src/vfs/VirtualFileSystem.ts index 3aa0db78..920622be 100644 --- a/src/vfs/VirtualFileSystem.ts +++ b/src/vfs/VirtualFileSystem.ts @@ -1610,28 +1610,19 @@ export class VirtualFileSystem implements IVirtualFileSystem { const entityId = await this.pathResolver.resolve(path) const results: Array<{ path: string, relationship: string, direction: 'from' | 'to' }> = [] - // Get all relationships involving this entity (both from and to) + // Use proper Brainy relationship API to get all relationships const [fromRelations, toRelations] = await Promise.all([ - this.brain.find({ - connected: { - from: entityId - }, - limit: 1000 - }), - this.brain.find({ - connected: { - to: entityId - }, - limit: 1000 - }) + this.brain.getRelations({ from: entityId }), + this.brain.getRelations({ to: entityId }) ]) // Add outgoing relationships for (const rel of fromRelations) { - if (rel.entity.metadata?.path) { + const targetEntity = await this.brain.get(rel.to) + if (targetEntity && targetEntity.metadata?.path) { results.push({ - path: rel.entity.metadata.path, - relationship: 'related', + path: targetEntity.metadata.path, + relationship: rel.type || 'related', direction: 'from' }) } @@ -1639,10 +1630,11 @@ export class VirtualFileSystem implements IVirtualFileSystem { // Add incoming relationships for (const rel of toRelations) { - if (rel.entity.metadata?.path) { + const sourceEntity = await this.brain.get(rel.from) + if (sourceEntity && sourceEntity.metadata?.path) { results.push({ - path: rel.entity.metadata.path, - relationship: 'related', + path: sourceEntity.metadata.path, + relationship: rel.type || 'related', direction: 'to' }) } @@ -1657,51 +1649,39 @@ export class VirtualFileSystem implements IVirtualFileSystem { const entityId = await this.pathResolver.resolve(path) const relationships: Relation[] = [] - // Get all relationships involving this entity (both from and to) + // Use proper Brainy relationship API const [fromRelations, toRelations] = await Promise.all([ - this.brain.find({ - connected: { - from: entityId - }, - limit: 1000 - }), - this.brain.find({ - connected: { - to: entityId - }, - limit: 1000 - }) + this.brain.getRelations({ from: entityId }), + this.brain.getRelations({ to: entityId }) ]) - // Add outgoing relationships (exclude parent-child relationships) + // Process outgoing relationships (excluding Contains for parent-child) for (const rel of fromRelations) { - if (rel.entity.metadata?.path && rel.entity.metadata?.vfsType) { - // Skip parent-child relationships to focus on user-defined relationships - const parentPath = this.getParentPath(rel.entity.metadata.path) - if (parentPath !== path) { // Not a direct child + if (rel.type !== VerbType.Contains) { // Skip filesystem hierarchy + const targetEntity = await this.brain.get(rel.to) + if (targetEntity && targetEntity.metadata?.path) { relationships.push({ - id: crypto.randomUUID(), - from: path, - to: rel.entity.metadata.path, - type: VerbType.References, - createdAt: Date.now() + id: rel.id || crypto.randomUUID(), + from: entityId, + to: rel.to, + type: rel.type, + createdAt: rel.createdAt || Date.now() }) } } } - // Add incoming relationships (exclude parent-child relationships) + // Process incoming relationships (excluding Contains for parent-child) for (const rel of toRelations) { - if (rel.entity.metadata?.path && rel.entity.metadata?.vfsType) { - // Skip parent-child relationships to focus on user-defined relationships - const parentPath = this.getParentPath(path) - if (rel.entity.metadata.path !== parentPath) { // Not the parent + if (rel.type !== VerbType.Contains) { // Skip filesystem hierarchy + const sourceEntity = await this.brain.get(rel.from) + if (sourceEntity && sourceEntity.metadata?.path) { relationships.push({ - id: crypto.randomUUID(), - from: rel.entity.metadata.path, - to: path, - type: VerbType.References, - createdAt: Date.now() + id: rel.id || crypto.randomUUID(), + from: rel.from, + to: entityId, + type: rel.type, + createdAt: rel.createdAt || Date.now() }) } } diff --git a/tests/vfs/vfs-relationships.test.ts b/tests/vfs/vfs-relationships.test.ts new file mode 100644 index 00000000..5b5bde77 --- /dev/null +++ b/tests/vfs/vfs-relationships.test.ts @@ -0,0 +1,195 @@ +/** + * Test VFS Graph Relationships + * + * Verifies that VFS properly uses Brainy's graph relationships + * instead of metadata-based path queries + */ + +import { describe, it, expect, beforeEach } from 'vitest' +import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js' +import { Brainy } from '../../src/brainy.js' +import { VerbType } from '../../src/types/graphTypes.js' + +describe('VFS Graph Relationships', () => { + let vfs: VirtualFileSystem + let brain: Brainy + + beforeEach(async () => { + brain = new Brainy() + await brain.init() + vfs = new VirtualFileSystem(brain) + await vfs.init() + }) + + it('should use proper graph relationships for directory structure', async () => { + // Create a directory structure + await vfs.mkdir('/projects') + await vfs.mkdir('/projects/brainy') + await vfs.writeFile('/projects/brainy/README.md', 'Test content') + await vfs.writeFile('/projects/brainy/package.json', '{}') + + // Get the entity IDs + const projectsId = await vfs.resolvePath('/projects') + const brainyId = await vfs.resolvePath('/projects/brainy') + const readmeId = await vfs.resolvePath('/projects/brainy/README.md') + + // Verify relationships are created properly + const projectRelations = await brain.getRelations({ + from: projectsId, + type: VerbType.Contains + }) + + expect(projectRelations).toHaveLength(1) + expect(projectRelations[0].to).toBe(brainyId) + + // Verify brainy directory contains its files + const brainyRelations = await brain.getRelations({ + from: brainyId, + type: VerbType.Contains + }) + + expect(brainyRelations).toHaveLength(2) + const childIds = brainyRelations.map(r => r.to) + expect(childIds).toContain(readmeId) + }) + + it('should traverse directory tree using relationships', async () => { + // Create nested structure + await vfs.mkdir('/a') + await vfs.mkdir('/a/b') + await vfs.mkdir('/a/b/c') + await vfs.writeFile('/a/b/c/file.txt', 'deep file') + + // Read directory using relationships + const contents = await vfs.readdir('/a/b/c') + expect(contents).toContain('file.txt') + + // Verify path resolution uses graph traversal + const fileId = await vfs.resolvePath('/a/b/c/file.txt') + const fileEntity = await brain.get(fileId) + expect(fileEntity?.metadata?.name).toBe('file.txt') + }) + + it('should properly handle custom relationships between files', async () => { + // Create two files + await vfs.writeFile('/doc1.md', 'Document 1') + await vfs.writeFile('/doc2.md', 'Document 2') + + // Add a custom relationship + await vfs.addRelationship('/doc1.md', '/doc2.md', VerbType.References) + + // Get relationships using proper graph API + const relationships = await vfs.getRelationships('/doc1.md') + + // Should find the reference relationship + const refRelation = relationships.find(r => r.type === VerbType.References) + expect(refRelation).toBeDefined() + + // Verify it's using actual graph relationships, not metadata + const doc1Id = await vfs.resolvePath('/doc1.md') + const doc2Id = await vfs.resolvePath('/doc2.md') + + const directRelations = await brain.getRelations({ + from: doc1Id, + type: VerbType.References + }) + + expect(directRelations).toHaveLength(1) + expect(directRelations[0].to).toBe(doc2Id) + }) + + it('should not fall back to metadata path queries', async () => { + // Create a file + await vfs.writeFile('/test.txt', 'test') + + // Get the entity + const entity = await vfs.getEntity('/test.txt') + + // Verify the entity has proper metadata + expect(entity.metadata.path).toBe('/test.txt') + expect(entity.metadata.name).toBe('test.txt') + + // But the parent relationship should be through graph, not metadata + const rootId = await vfs.resolvePath('/') + const testId = entity.id + + // Check that root contains test.txt via relationships + const rootRelations = await brain.getRelations({ + from: rootId, + type: VerbType.Contains + }) + + expect(rootRelations.some(r => r.to === testId)).toBe(true) + }) + + it('should efficiently query children using relationships', async () => { + // Create many files in a directory + await vfs.mkdir('/many') + for (let i = 0; i < 10; i++) { + await vfs.writeFile(`/many/file${i}.txt`, `content ${i}`) + } + + // Get directory contents + const files = await vfs.readdir('/many') + expect(files).toHaveLength(10) + + // Verify it's using relationships, not metadata queries + const manyId = await vfs.resolvePath('/many') + const relations = await brain.getRelations({ + from: manyId, + type: VerbType.Contains + }) + + expect(relations).toHaveLength(10) + }) + + it('should handle moving files by updating relationships', async () => { + // Create source structure + await vfs.mkdir('/source') + await vfs.writeFile('/source/file.txt', 'content') + await vfs.mkdir('/dest') + + // Move file + await vfs.move('/source/file.txt', '/dest/file.txt') + + // Verify relationships are updated + const sourceId = await vfs.resolvePath('/source') + const destId = await vfs.resolvePath('/dest') + const fileId = await vfs.resolvePath('/dest/file.txt') + + // Source should not contain file anymore + const sourceRelations = await brain.getRelations({ + from: sourceId, + type: VerbType.Contains + }) + expect(sourceRelations).toHaveLength(0) + + // Dest should contain file + const destRelations = await brain.getRelations({ + from: destId, + type: VerbType.Contains + }) + expect(destRelations).toHaveLength(1) + expect(destRelations[0].to).toBe(fileId) + }) + + it('should support complex graph queries', async () => { + // Create interconnected structure + await vfs.mkdir('/docs') + await vfs.writeFile('/docs/main.md', 'Main doc') + await vfs.writeFile('/docs/related1.md', 'Related 1') + await vfs.writeFile('/docs/related2.md', 'Related 2') + + // Add cross-references + await vfs.addRelationship('/docs/main.md', '/docs/related1.md', VerbType.References) + await vfs.addRelationship('/docs/main.md', '/docs/related2.md', VerbType.References) + await vfs.addRelationship('/docs/related1.md', '/docs/related2.md', VerbType.References) + + // Get all related documents + const related = await vfs.getRelated('/docs/main.md') + + // Should find parent (Contains) and references + const references = related.filter(r => r.direction === 'from') + expect(references.length).toBeGreaterThanOrEqual(2) + }) +}) \ No newline at end of file