feat: refactor VFS to use proper Brainy graph relationships
- Replace metadata path queries with brain.getRelations() API - Use graph traversal for parent-child relationships via VerbType.Contains - Remove fallback metadata queries - trust graph relationships completely - Fix getChildren() to properly query relationship graph - Update getRelated() and getRelationships() to use native Brainy APIs This enables full graph benefits: indexes, optimizations, and visualizations now work correctly with VFS. The filesystem structure is now a true graph using Brainy's native relationship system. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cc6fa00f30
commit
4f76dac7be
3 changed files with 255 additions and 97 deletions
|
|
@ -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<string | null> {
|
||||
// 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<VFSEntity[]> {
|
||||
// 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<string>()
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
195
tests/vfs/vfs-relationships.test.ts
Normal file
195
tests/vfs/vfs-relationships.test.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue