8.0 RC cleanup toward "one place per thing, zero-config, no deprecation":
- Remove the `brain.neural()` clustering namespace (ImprovedNeuralAPI + the dead
legacy NeuralAPI + the neural CLI + neural-only types). Similarity is `find({vector})`
/ `similar({to})`; attribute grouping is the aggregation `GROUP BY` engine. The separate
entity-extraction / smart-import feature (NeuralImport, NeuralEntityExtractor, SmartExtractor,
NaturalLanguageProcessor, `brain.extract()`/`brain.nlp()`) is kept.
- Remove `Db.search()`; `find()` is the one query verb (accepts a bare string or FindParams).
Fix the bundled MCP client, which called a non-existent `brain.search(query, limit)` →
now `find({ query, limit })`.
- Storage config: collapse to one canonical top-level `path` key. The pre-8.0 aliases
(`rootDirectory`, `options.*`, `fileSystemStorage.*`) are removed and now THROW with the
exact rename instead of silently defaulting to `./brainy-data` on upgrade. A single resolver
feeds createStorage, the 7.x→8.0 migration probe, and the plugin-factory handoff, so a native
storage provider resolves the identical root (no split-brain).
- Fix `similar({ threshold })`: the min-similarity filter was silently dropped; it is now
applied as a post-filter on `result.score` (the documented way to bound semantic results).
- Fix `vfs.rename()` on a directory: child path updates spread the entity vector into `update()`
and failed dimension validation; they are metadata-only updates now.
- Fix `vfs.move()`: copy+delete orphaned the content-addressed content blob (the destination
shared the source hash, then unlink removed it). `move()` now delegates to `rename()` — an
in-place path change that preserves the blob and the entity id, for files and directories.
- Fix streaming import: the bulk fast path never flushed mid-import nor signalled queryability.
Entity writes are now chunked by a progressive flush interval (100 → 1000 → 5000); each chunk
flushes and emits `progress.queryable`, so imported data is queryable during the import.
- Sweep all docs, comments, and JSDoc for the removed/changed APIs.
Integration suite: 49 files / 588 passed / 0 failed. Unit: 80 files / 1456 passed, no type errors.
204 lines
7.2 KiB
TypeScript
204 lines
7.2 KiB
TypeScript
/**
|
|
* VFS-Knowledge Separation Test (8.0)
|
|
*
|
|
* Exercises how VFS infrastructure entities coexist with knowledge-graph
|
|
* entities in the same brain, and how a query opts in/out of the VFS layer:
|
|
*
|
|
* - VFS files/directories are real entities, marked in metadata with
|
|
* `isVFS: true` / `isVFSEntity: true` and `vfsType: 'file' | 'directory'`.
|
|
* - `brain.find()` INCLUDES VFS entities by default (8.0 semantics).
|
|
* - `brain.find({ excludeVFS: true })` drops the VFS infrastructure layer,
|
|
* returning only knowledge entities — works on both the metadata-filter
|
|
* path and the vector/`query` path.
|
|
* - `where: { vfsType: 'file' }` selects VFS file entities explicitly.
|
|
* - Relationships can span VFS files and knowledge entities.
|
|
*
|
|
* Runs under the deterministic embedder (tests/setup-integration.ts), so
|
|
* cross-text semantic ranking is not meaningful; self-retrieval (querying an
|
|
* entity by its own text) and `excludeVFS` filtering are.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
import { Brainy } from '../../src/brainy.js'
|
|
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
|
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
|
|
describe('VFS-Knowledge Separation (8.0)', () => {
|
|
const testDir = path.join(process.cwd(), 'test-vfs-knowledge-separation')
|
|
let brain: Brainy
|
|
|
|
beforeAll(async () => {
|
|
// Clean up
|
|
if (fs.existsSync(testDir)) {
|
|
fs.rmSync(testDir, { recursive: true, force: true })
|
|
}
|
|
fs.mkdirSync(testDir, { recursive: true })
|
|
|
|
brain = new Brainy({
|
|
requireSubtype: false,
|
|
storage: {
|
|
type: 'filesystem',
|
|
path: testDir
|
|
}
|
|
})
|
|
await brain.init()
|
|
|
|
// Seed the VFS layer + one knowledge entity used across the suite.
|
|
const vfs = brain.vfs
|
|
await vfs.init()
|
|
await vfs.mkdir('/docs', { recursive: true })
|
|
await vfs.writeFile('/docs/readme.md', '# Hello World')
|
|
|
|
await brain.add({
|
|
data: 'This is a knowledge document about AI',
|
|
type: NounType.Document,
|
|
metadata: {
|
|
title: 'AI Research Paper',
|
|
category: 'research'
|
|
}
|
|
})
|
|
})
|
|
|
|
afterAll(async () => {
|
|
// Close releases the writer lock + flushes; prevents background-flush bleed.
|
|
await brain.close()
|
|
if (fs.existsSync(testDir)) {
|
|
fs.rmSync(testDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('includes VFS entities in brain.find() by default', async () => {
|
|
// 8.0: VFS infrastructure entities are returned by default. The /docs/readme.md
|
|
// file is a Document-typed entity carrying isVFS/vfsType markers, so a plain
|
|
// type query surfaces BOTH it and the knowledge document.
|
|
const results = await brain.find({
|
|
type: NounType.Document,
|
|
limit: 100
|
|
})
|
|
|
|
const vfsResults = results.filter((r) => r.metadata?.isVFS === true)
|
|
const knowledgeResults = results.filter((r) => r.metadata?.isVFS !== true)
|
|
|
|
// Default find() shows the VFS file alongside knowledge.
|
|
expect(vfsResults.length).toBeGreaterThan(0)
|
|
expect(knowledgeResults.length).toBeGreaterThan(0)
|
|
expect(results.some((r) => r.metadata?.title === 'AI Research Paper')).toBe(true)
|
|
expect(results.some((r) => r.metadata?.vfsType === 'file')).toBe(true)
|
|
})
|
|
|
|
it('excludes VFS entities when excludeVFS: true', async () => {
|
|
// 8.0: excludeVFS drops the VFS infrastructure layer. Only knowledge entities remain.
|
|
const results = await brain.find({
|
|
type: NounType.Document,
|
|
excludeVFS: true,
|
|
limit: 100
|
|
})
|
|
|
|
const vfsResults = results.filter((r) => r.metadata?.isVFS === true)
|
|
const knowledgeResults = results.filter((r) => r.metadata?.isVFS !== true)
|
|
|
|
expect(vfsResults.length).toBe(0)
|
|
expect(knowledgeResults.length).toBeGreaterThan(0)
|
|
expect(results.some((r) => r.metadata?.title === 'AI Research Paper')).toBe(true)
|
|
})
|
|
|
|
it('should allow relationships between VFS files and knowledge entities', async () => {
|
|
// Get VFS file entity. VFS files are identified by vfsType (the indexed,
|
|
// queryable marker); they are included in find() by default.
|
|
const vfsFile = await brain.find({
|
|
where: {
|
|
path: '/docs/readme.md'
|
|
},
|
|
limit: 1
|
|
})
|
|
expect(vfsFile.length).toBe(1)
|
|
expect(vfsFile[0].metadata?.vfsType).toBe('file')
|
|
|
|
// Get knowledge entity
|
|
const knowledgeEntity = await brain.find({
|
|
type: NounType.Document,
|
|
where: {
|
|
title: 'AI Research Paper'
|
|
},
|
|
excludeVFS: true,
|
|
limit: 1
|
|
})
|
|
expect(knowledgeEntity.length).toBe(1)
|
|
|
|
// Create relationship: knowledge entity -> references -> VFS file.
|
|
// relate() returns the new relation's id (string).
|
|
const relationId = await brain.relate({
|
|
from: knowledgeEntity[0].id,
|
|
to: vfsFile[0].id,
|
|
type: VerbType.References
|
|
})
|
|
expect(typeof relationId).toBe('string')
|
|
expect(relationId.length).toBeGreaterThan(0)
|
|
|
|
// Verify relationship exists
|
|
const relations = await brain.related({
|
|
from: knowledgeEntity[0].id,
|
|
to: vfsFile[0].id
|
|
})
|
|
|
|
expect(relations.length).toBe(1)
|
|
expect(relations[0].type).toBe(VerbType.References)
|
|
})
|
|
|
|
it('should filter VFS entities using where clause', async () => {
|
|
// Query for VFS files explicitly via the vfsType marker (indexed + queryable).
|
|
const vfsFiles = await brain.find({
|
|
where: {
|
|
vfsType: 'file'
|
|
},
|
|
limit: 100
|
|
})
|
|
|
|
expect(vfsFiles.length).toBeGreaterThan(0)
|
|
expect(vfsFiles.every((f) => f.metadata?.vfsType === 'file')).toBe(true)
|
|
// Every vfsType:'file' entity is a VFS infrastructure entity.
|
|
expect(vfsFiles.every((f) => f.metadata?.isVFS === true)).toBe(true)
|
|
|
|
// Query for non-VFS knowledge entities explicitly via a user metadata field.
|
|
const nonVFS = await brain.find({
|
|
where: {
|
|
category: 'research'
|
|
},
|
|
limit: 100
|
|
})
|
|
|
|
expect(nonVFS.length).toBeGreaterThan(0)
|
|
expect(nonVFS.every((e) => e.metadata?.isVFS !== true)).toBe(true)
|
|
expect(nonVFS.every((e) => e.metadata?.vfsType === undefined)).toBe(true)
|
|
})
|
|
|
|
it('should handle semantic search with VFS filtering', async () => {
|
|
// Self-retrieval: querying with the knowledge doc's own text returns it
|
|
// (deterministic embedder ⇒ cosine 1.0). With excludeVFS the VFS layer is dropped.
|
|
const knowledgeOnly = await brain.find({
|
|
query: 'This is a knowledge document about AI',
|
|
excludeVFS: true,
|
|
limit: 10
|
|
})
|
|
expect(knowledgeOnly.length).toBeGreaterThan(0)
|
|
expect(knowledgeOnly.some((r) => r.metadata?.isVFS === true)).toBe(false)
|
|
expect(knowledgeOnly.some((r) => r.metadata?.title === 'AI Research Paper')).toBe(true)
|
|
|
|
// Default (no excludeVFS): self-retrieval of the VFS file by its own content
|
|
// returns it — VFS entities participate in vector search by default.
|
|
const withVFS = await brain.find({
|
|
query: '# Hello World',
|
|
limit: 10
|
|
})
|
|
expect(withVFS.some((r) => r.metadata?.path === '/docs/readme.md')).toBe(true)
|
|
|
|
// excludeVFS on the vector path removes the VFS file even when its content matches.
|
|
const withVFSExcluded = await brain.find({
|
|
query: '# Hello World',
|
|
excludeVFS: true,
|
|
limit: 10
|
|
})
|
|
expect(withVFSExcluded.some((r) => r.metadata?.isVFS === true)).toBe(false)
|
|
})
|
|
})
|