feat(8.0): API simplification — remove neural()/Db.search, one storage path key, integration→0
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.
This commit is contained in:
parent
0c4a51c24e
commit
606445cd61
74 changed files with 712 additions and 7470 deletions
|
|
@ -1,53 +0,0 @@
|
|||
/**
|
||||
* Storage root-directory resolution from `StorageOptions`.
|
||||
*
|
||||
* `storage: { type: 'filesystem', path: '…' }` is a widely-used, doc-promoted
|
||||
* config shape. A refactor once dropped the top-level `path` key from the
|
||||
* resolution chain, so it was silently ignored and every brain wrote to the
|
||||
* default `./brainy-data` instead — a quiet data-misplacement footgun on
|
||||
* upgrade. These tests pin every accepted spelling to the directory it must
|
||||
* resolve to.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { createStorage } from '../../../src/storage/storageFactory.js'
|
||||
|
||||
/** Read the resolved root directory off the concrete FileSystemStorage. */
|
||||
function rootDirOf(storage: unknown): string {
|
||||
return (storage as { rootDir: string }).rootDir
|
||||
}
|
||||
|
||||
describe('createStorage — filesystem root-directory resolution', () => {
|
||||
it('honors top-level rootDirectory', async () => {
|
||||
const storage = await createStorage({ type: 'filesystem', rootDirectory: '/tmp/brainy-rd' })
|
||||
expect(rootDirOf(storage)).toBe('/tmp/brainy-rd')
|
||||
})
|
||||
|
||||
it('honors top-level path (the documented shorthand)', async () => {
|
||||
const storage = await createStorage({ type: 'filesystem', path: '/tmp/brainy-path' })
|
||||
expect(rootDirOf(storage)).toBe('/tmp/brainy-path')
|
||||
})
|
||||
|
||||
it('honors nested options.rootDirectory', async () => {
|
||||
const storage = await createStorage({ type: 'filesystem', options: { rootDirectory: '/tmp/brainy-ord' } })
|
||||
expect(rootDirOf(storage)).toBe('/tmp/brainy-ord')
|
||||
})
|
||||
|
||||
it('honors nested options.path', async () => {
|
||||
const storage = await createStorage({ type: 'filesystem', options: { path: '/tmp/brainy-opath' } })
|
||||
expect(rootDirOf(storage)).toBe('/tmp/brainy-opath')
|
||||
})
|
||||
|
||||
it('prefers top-level rootDirectory over a nested options.path', async () => {
|
||||
const storage = await createStorage({
|
||||
type: 'filesystem',
|
||||
rootDirectory: '/tmp/brainy-win',
|
||||
options: { path: '/tmp/brainy-lose' }
|
||||
})
|
||||
expect(rootDirOf(storage)).toBe('/tmp/brainy-win')
|
||||
})
|
||||
|
||||
it('falls back to ./brainy-data only when no directory is supplied', async () => {
|
||||
const storage = await createStorage({ type: 'filesystem' })
|
||||
expect(rootDirOf(storage)).toBe('./brainy-data')
|
||||
})
|
||||
})
|
||||
171
tests/unit/storage/storage-path-resolution.test.ts
Normal file
171
tests/unit/storage/storage-path-resolution.test.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
/**
|
||||
* @module tests/unit/storage/storage-path-resolution
|
||||
* @description Acceptance suite for the consolidated filesystem storage-path
|
||||
* surface (8.0). Brainy resolves the on-disk root through ONE function,
|
||||
* {@link resolveFilesystemRoot}. 8.0 is a clean break: `path` is the ONE
|
||||
* supported key (the rest of the API already speaks it: `persist(path)`,
|
||||
* `Brainy.load(path)`, `asOf(path)`, `restore(path)`). The pre-8.0 aliases
|
||||
* (`rootDirectory`, `options.*`, `fileSystemStorage.*`) were REMOVED and now
|
||||
* THROW with the rename — never a silent default that would misplace data.
|
||||
* Three things must hold and are pinned here:
|
||||
* 1. CLEAN BREAK — `path` resolves; any removed alias throws.
|
||||
* 2. TYPE INFERENCE — a bare `path` (no `type`) implies filesystem.
|
||||
* 3. COR-SAFETY — a plugin storage factory receives the RESOLVED `path`, so a
|
||||
* native side (mmap / getBinaryBlobPath) can never split-brain onto a
|
||||
* different directory than the one brainy itself uses.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import {
|
||||
resolveFilesystemRoot,
|
||||
createStorage,
|
||||
DEFAULT_FILESYSTEM_ROOT
|
||||
} from '../../../src/storage/storageFactory.js'
|
||||
import { Brainy } from '../../../src/brainy.js'
|
||||
import type { BrainyPlugin, BrainyPluginContext, StorageAdapterFactory } from '../../../src/plugin.js'
|
||||
import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js'
|
||||
import type { StorageAdapter } from '../../../src/coreTypes.js'
|
||||
|
||||
/** Read the resolved root directory off a concrete FileSystemStorage. */
|
||||
function rootDirOf(storage: unknown): string {
|
||||
return (storage as { rootDir: string }).rootDir
|
||||
}
|
||||
|
||||
const TARGET = '/tmp/brainy-resolve-target'
|
||||
|
||||
describe('resolveFilesystemRoot — clean break: path resolves, removed aliases throw', () => {
|
||||
it('resolves the canonical top-level path', () => {
|
||||
expect(resolveFilesystemRoot({ path: TARGET })).toBe(TARGET)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['rootDirectory', { rootDirectory: TARGET }],
|
||||
['options.path', { options: { path: TARGET } }],
|
||||
['options.rootDirectory', { options: { rootDirectory: TARGET } }],
|
||||
['fileSystemStorage.path', { fileSystemStorage: { path: TARGET } }],
|
||||
['fileSystemStorage.rootDirectory', { fileSystemStorage: { rootDirectory: TARGET } }]
|
||||
])('throws (naming `path`) for the removed alias %s', (_label, config) => {
|
||||
expect(() => resolveFilesystemRoot(config as any)).toThrow(/removed in 8\.0|'path'/)
|
||||
})
|
||||
|
||||
it('a removed alias throws rather than silently falling through to the default', () => {
|
||||
// The footgun this guards: a 7.x `{ rootDirectory }` config must NOT land
|
||||
// on ./brainy-data on upgrade — it must fail loudly with the rename.
|
||||
expect(() => resolveFilesystemRoot({ rootDirectory: TARGET } as any)).toThrow(/'path'/)
|
||||
})
|
||||
|
||||
it('the canonical path wins and does NOT throw even if a stale alias is also present', () => {
|
||||
expect(resolveFilesystemRoot({ path: '/win', rootDirectory: '/lose' } as any)).toBe('/win')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveFilesystemRoot — zero-config default', () => {
|
||||
it('returns ./brainy-data when no directory is supplied', () => {
|
||||
expect(resolveFilesystemRoot({})).toBe(DEFAULT_FILESYSTEM_ROOT)
|
||||
expect(resolveFilesystemRoot({})).toBe('./brainy-data')
|
||||
})
|
||||
|
||||
it('returns ./brainy-data for type:filesystem with no path ("persist, default location")', () => {
|
||||
expect(resolveFilesystemRoot({ type: 'filesystem' })).toBe('./brainy-data')
|
||||
})
|
||||
|
||||
it('ignores empty-string paths and falls through to the default', () => {
|
||||
expect(resolveFilesystemRoot({ path: '' } as any)).toBe('./brainy-data')
|
||||
})
|
||||
})
|
||||
|
||||
describe('createStorage — type inference (path implies filesystem)', () => {
|
||||
it('a bare { path } (no type) produces a FileSystemStorage at that path', async () => {
|
||||
const storage = await createStorage({ path: TARGET })
|
||||
expect(storage.constructor.name).toBe('FileSystemStorage')
|
||||
expect(rootDirOf(storage)).toBe(TARGET)
|
||||
})
|
||||
|
||||
it('a bare { rootDirectory } (removed alias) throws via createStorage', async () => {
|
||||
await expect(createStorage({ rootDirectory: TARGET } as any)).rejects.toThrow(
|
||||
/removed in 8\.0|'path'/
|
||||
)
|
||||
})
|
||||
|
||||
it('an explicit type:filesystem with no path lands on ./brainy-data', async () => {
|
||||
const storage = await createStorage({ type: 'filesystem' })
|
||||
expect(storage.constructor.name).toBe('FileSystemStorage')
|
||||
expect(rootDirOf(storage)).toBe('./brainy-data')
|
||||
})
|
||||
|
||||
it('type:memory always produces MemoryStorage regardless of path', async () => {
|
||||
const storage = await createStorage({ type: 'memory', path: TARGET } as any)
|
||||
expect(storage.constructor.name).toBe('MemoryStorage')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* A boundary-safe fake plugin storage factory. It is NOT @soulcraft/cor — it
|
||||
* stands in for any native/plugin storage backend that re-resolves the on-disk
|
||||
* directory itself. It records the EXACT config object handed to `create()` so
|
||||
* the test can assert brainy normalized the canonical `path` before the handoff.
|
||||
*/
|
||||
class RecordingStorageFactory implements StorageAdapterFactory {
|
||||
name = 'recording-filesystem'
|
||||
received: Record<string, unknown> | null = null
|
||||
|
||||
create(config: Record<string, unknown>): StorageAdapter {
|
||||
this.received = config
|
||||
return new MemoryStorage() as unknown as StorageAdapter
|
||||
}
|
||||
}
|
||||
|
||||
/** A minimal plugin that registers the recording factory under `storage:filesystem`. */
|
||||
function makeRecordingPlugin(factory: RecordingStorageFactory): BrainyPlugin {
|
||||
return {
|
||||
name: 'test-recording-storage',
|
||||
async activate(ctx: BrainyPluginContext): Promise<boolean> {
|
||||
ctx.registerProvider('storage:filesystem', factory)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('cor-safety — plugin storage factory receives the RESOLVED path', () => {
|
||||
const brains: Brainy[] = []
|
||||
afterEach(async () => {
|
||||
for (const b of brains.splice(0)) {
|
||||
try {
|
||||
await b.close()
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('normalizes a canonical top-level { path } before handing it to the factory', async () => {
|
||||
const factory = new RecordingStorageFactory()
|
||||
const brain = new Brainy({
|
||||
requireSubtype: false,
|
||||
silent: true,
|
||||
storage: { type: 'filesystem', path: TARGET }
|
||||
})
|
||||
brain.use(makeRecordingPlugin(factory))
|
||||
brains.push(brain)
|
||||
await brain.init()
|
||||
|
||||
expect(factory.received).not.toBeNull()
|
||||
expect(factory.received!.path).toBe(TARGET)
|
||||
})
|
||||
|
||||
it('a removed { rootDirectory } alias throws at init and never reaches the factory', async () => {
|
||||
// Clean break: the normalize step (resolveFilesystemRoot) throws on the
|
||||
// removed alias BEFORE the plugin factory is ever called — no split-brain,
|
||||
// no silent ./brainy-data.
|
||||
const factory = new RecordingStorageFactory()
|
||||
const brain = new Brainy({
|
||||
requireSubtype: false,
|
||||
silent: true,
|
||||
storage: { type: 'filesystem', rootDirectory: TARGET }
|
||||
})
|
||||
brain.use(makeRecordingPlugin(factory))
|
||||
brains.push(brain)
|
||||
|
||||
await expect(brain.init()).rejects.toThrow(/removed in 8\.0|'path'/)
|
||||
expect(factory.received).toBeNull()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue