2025-11-18 16:06:34 -08:00
/ * *
2026-06-17 13:11:41 -07:00
* Comprehensive Metadata - Only Integration Test
2025-11-18 16:06:34 -08:00
*
2026-06-17 13:11:41 -07:00
* Verifies the 8.0 metadata - only read model works across subsystems :
* - Storage adapters ( memory , filesystem )
2025-11-18 16:06:34 -08:00
* - Indexes ( Metadata , Graph , HNSW )
2026-06-17 13:11:41 -07:00
* - APIs ( find , update , remove , related , similar )
* - VFS operations ( read / stat / readdir )
*
* Reminder ( 8.0 ) : brain . get ( id ) loads metadata only — entity . vector is [ ] .
* Pass { includeVectors : true } when you need the embedding . find ( ) returns
* Result [ ] ( flattened metadata + full entity ) ; vectors are not on the Result .
2025-11-18 16:06:34 -08:00
* /
import { describe , it , expect , beforeEach , afterEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { VirtualFileSystem } from '../../src/vfs/VirtualFileSystem.js'
2026-06-17 12:19:16 -07:00
import { NounType , VerbType } from '../../src/types/graphTypes.js'
2025-11-18 16:06:34 -08:00
import { mkdtempSync , rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
2026-06-17 13:11:41 -07:00
describe ( 'Metadata-Only Comprehensive Integration' , ( ) = > {
2025-11-18 16:06:34 -08:00
describe ( 'Storage Adapters' , ( ) = > {
it ( 'should work with MemoryStorage' , async ( ) = > {
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.
OPT-OUT REMAINS FULLY SUPPORTED
The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:
- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
contract entirely. Recommended only for migration windows or test
fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
optional vocabulary. Composes with the brain-wide flag.
Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.
TEST SWEEP
Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
- `new Brainy({` → `new Brainy({ requireSubtype: false,`
- `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
- `new Brainy()` → `new Brainy({ requireSubtype: false })`
tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.
The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.
CHANGES
src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
Comment refreshed to document the three opt-out paths.
tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
opt-out preserves the test author's original intent.
tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.
NO-OP for consumers who were already passing subtype on every write.
For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
no other regressions from the flip)
2026-06-09 14:58:25 -07:00
const brain = new Brainy ( { requireSubtype : false ,
2026-06-17 13:11:41 -07:00
storage : { type : 'memory' } ,
2025-11-18 16:06:34 -08:00
silent : true
} )
await brain . init ( )
const id = await brain . add ( {
data : 'Test data' ,
type : NounType . Document ,
metadata : { title : 'Test' }
} )
// Metadata-only (default)
const entity = await brain . get ( id )
expect ( entity ) . toBeTruthy ( )
expect ( entity ! . data ) . toBe ( 'Test data' )
expect ( entity ! . metadata . title ) . toBe ( 'Test' )
expect ( entity ! . vector ) . toEqual ( [ ] ) // No vectors loaded
// Full entity
const full = await brain . get ( id , { includeVectors : true } )
expect ( full ! . vector . length ) . toBe ( 384 )
await brain . close ( )
} )
it ( 'should work with FileSystemStorage' , async ( ) = > {
const testDir = mkdtempSync ( join ( tmpdir ( ) , 'brainy-metadata-test-' ) )
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.
OPT-OUT REMAINS FULLY SUPPORTED
The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:
- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
contract entirely. Recommended only for migration windows or test
fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
optional vocabulary. Composes with the brain-wide flag.
Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.
TEST SWEEP
Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
- `new Brainy({` → `new Brainy({ requireSubtype: false,`
- `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
- `new Brainy()` → `new Brainy({ requireSubtype: false })`
tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.
The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.
CHANGES
src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
Comment refreshed to document the three opt-out paths.
tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
opt-out preserves the test author's original intent.
tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.
NO-OP for consumers who were already passing subtype on every write.
For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
no other regressions from the flip)
2026-06-09 14:58:25 -07:00
const brain = new Brainy ( { requireSubtype : false ,
2025-11-18 16:06:34 -08:00
storage : {
type : 'filesystem' ,
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.
2026-06-20 13:31:11 -07:00
path : testDir
2025-11-18 16:06:34 -08:00
} ,
silent : true
} )
await brain . init ( )
const id = await brain . add ( {
data : 'FS test data' ,
type : NounType . File ,
metadata : { filename : 'test.txt' }
} )
// Metadata-only
const entity = await brain . get ( id )
expect ( entity ! . metadata . filename ) . toBe ( 'test.txt' )
expect ( entity ! . vector ) . toEqual ( [ ] )
// Full entity
const full = await brain . get ( id , { includeVectors : true } )
expect ( full ! . vector . length ) . toBe ( 384 )
await brain . close ( )
rmSync ( testDir , { recursive : true , force : true } )
} )
} )
describe ( 'Indexes' , ( ) = > {
let brain : Brainy
beforeEach ( async ( ) = > {
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.
OPT-OUT REMAINS FULLY SUPPORTED
The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:
- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
contract entirely. Recommended only for migration windows or test
fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
optional vocabulary. Composes with the brain-wide flag.
Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.
TEST SWEEP
Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
- `new Brainy({` → `new Brainy({ requireSubtype: false,`
- `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
- `new Brainy()` → `new Brainy({ requireSubtype: false })`
tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.
The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.
CHANGES
src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
Comment refreshed to document the three opt-out paths.
tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
opt-out preserves the test author's original intent.
tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.
NO-OP for consumers who were already passing subtype on every write.
For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
no other regressions from the flip)
2026-06-09 14:58:25 -07:00
brain = new Brainy ( { requireSubtype : false ,
2026-06-17 13:11:41 -07:00
storage : { type : 'memory' } ,
2025-11-18 16:06:34 -08:00
silent : true
} )
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
it ( 'should work with MetadataIndex (find with where)' , async ( ) = > {
await brain . add ( {
data : 'Product 1' ,
type : NounType . Product ,
metadata : { price : 100 , category : 'electronics' }
} )
await brain . add ( {
data : 'Product 2' ,
type : NounType . Product ,
metadata : { price : 200 , category : 'electronics' }
} )
const results = await brain . find ( {
where : { category : 'electronics' , price : 100 }
} )
expect ( results . length ) . toBe ( 1 )
expect ( results [ 0 ] . metadata . price ) . toBe ( 100 )
2026-06-17 13:11:41 -07:00
// 8.0: find() Result is metadata-flattened; vectors are not part of the
// Result shape. The metadata/where filter is what this test verifies.
expect ( results [ 0 ] . metadata . category ) . toBe ( 'electronics' )
2025-11-18 16:06:34 -08:00
} )
it ( 'should work with GraphAdjacencyIndex (relationships)' , async ( ) = > {
const alice = await brain . add ( {
data : 'Alice' ,
type : NounType . Person
} )
const bob = await brain . add ( {
data : 'Bob' ,
type : NounType . Person
} )
2026-06-17 12:19:16 -07:00
await brain . relate ( { from : alice , to : bob , type : VerbType . Knows } )
2025-11-18 16:06:34 -08:00
2026-06-17 13:11:41 -07:00
// related() reads the graph adjacency index (8.0 replaces getVerbsBySource)
const relationships = await brain . related ( { from : alice } )
2025-11-18 16:06:34 -08:00
expect ( relationships . length ) . toBe ( 1 )
2026-06-17 13:11:41 -07:00
expect ( relationships [ 0 ] . to ) . toBe ( bob )
expect ( relationships [ 0 ] . type ) . toBe ( VerbType . Knows )
2025-11-18 16:06:34 -08:00
} )
it ( 'should work with HNSW (vector similarity)' , async ( ) = > {
const id = await brain . add ( {
data : 'Machine learning tutorial' ,
type : NounType . Document
} )
// brain.similar uses HNSW index
const results = await brain . similar ( { to : id , limit : 5 } )
expect ( results ) . toBeDefined ( )
expect ( Array . isArray ( results ) ) . toBe ( true )
} )
} )
describe ( 'Core APIs' , ( ) = > {
let brain : Brainy
let entityId : string
beforeEach ( async ( ) = > {
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.
OPT-OUT REMAINS FULLY SUPPORTED
The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:
- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
contract entirely. Recommended only for migration windows or test
fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
optional vocabulary. Composes with the brain-wide flag.
Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.
TEST SWEEP
Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
- `new Brainy({` → `new Brainy({ requireSubtype: false,`
- `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
- `new Brainy()` → `new Brainy({ requireSubtype: false })`
tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.
The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.
CHANGES
src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
Comment refreshed to document the three opt-out paths.
tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
opt-out preserves the test author's original intent.
tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.
NO-OP for consumers who were already passing subtype on every write.
For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
no other regressions from the flip)
2026-06-09 14:58:25 -07:00
brain = new Brainy ( { requireSubtype : false ,
2026-06-17 13:11:41 -07:00
storage : { type : 'memory' } ,
2025-11-18 16:06:34 -08:00
silent : true
} )
await brain . init ( )
entityId = await brain . add ( {
data : 'Test entity' ,
type : NounType . Thing ,
metadata : { value : 'original' }
} )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
it ( 'brain.update() should work with metadata-only get' , async ( ) = > {
await brain . update ( {
id : entityId ,
metadata : { value : 'updated' }
} )
const entity = await brain . get ( entityId )
expect ( entity ! . metadata . value ) . toBe ( 'updated' )
expect ( entity ! . vector ) . toEqual ( [ ] ) // Still metadata-only
} )
2026-06-11 14:51:00 -07:00
it ( 'brain.remove() should work after metadata-only get' , async ( ) = > {
2025-11-18 16:06:34 -08:00
const entity = await brain . get ( entityId )
expect ( entity ) . toBeTruthy ( )
2026-06-11 14:51:00 -07:00
await brain . remove ( entityId )
2025-11-18 16:06:34 -08:00
const deleted = await brain . get ( entityId )
expect ( deleted ) . toBeNull ( )
} )
2026-06-17 13:11:41 -07:00
it ( 'brain.find() should return results with flattened metadata + entity' , async ( ) = > {
2025-11-18 16:06:34 -08:00
const results = await brain . find ( {
type : NounType . Thing ,
limit : 10
} )
expect ( results . length ) . toBeGreaterThan ( 0 )
2026-06-17 13:11:41 -07:00
// 8.0: find() returns Result[] with common entity fields flattened to the
// top level (id/type/metadata) plus the full entity under `entity`.
// Vectors are intentionally NOT part of the Result shape.
const hit = results . find ( r = > r . id === entityId )
expect ( hit ) . toBeTruthy ( )
expect ( hit ! . type ) . toBe ( NounType . Thing )
expect ( hit ! . metadata . value ) . toBe ( 'original' )
expect ( hit ! . entity . id ) . toBe ( entityId )
2025-11-18 16:06:34 -08:00
} )
it ( 'brain.similar() should work with entity ID' , async ( ) = > {
const results = await brain . similar ( { to : entityId , limit : 5 } )
expect ( results ) . toBeDefined ( )
expect ( Array . isArray ( results ) ) . toBe ( true )
} )
it ( 'brain.similar() should reject metadata-only entities' , async ( ) = > {
const entity = await brain . get ( entityId ) // metadata-only
await expect (
brain . similar ( { to : entity ! , limit : 5 } )
) . rejects . toThrow ( 'no vector embeddings loaded' )
} )
it ( 'brain.similar() should work with full entities' , async ( ) = > {
const entity = await brain . get ( entityId , { includeVectors : true } )
const results = await brain . similar ( { to : entity ! , limit : 5 } )
expect ( results ) . toBeDefined ( )
expect ( Array . isArray ( results ) ) . toBe ( true )
} )
} )
describe ( 'VFS Integration' , ( ) = > {
let brain : Brainy
let vfs : VirtualFileSystem
let testDir : string
beforeEach ( async ( ) = > {
testDir = mkdtempSync ( join ( tmpdir ( ) , 'brainy-vfs-metadata-test-' ) )
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.
OPT-OUT REMAINS FULLY SUPPORTED
The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:
- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
contract entirely. Recommended only for migration windows or test
fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
optional vocabulary. Composes with the brain-wide flag.
Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.
TEST SWEEP
Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
- `new Brainy({` → `new Brainy({ requireSubtype: false,`
- `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
- `new Brainy()` → `new Brainy({ requireSubtype: false })`
tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.
The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.
CHANGES
src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
Comment refreshed to document the three opt-out paths.
tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
opt-out preserves the test author's original intent.
tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.
NO-OP for consumers who were already passing subtype on every write.
For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
no other regressions from the flip)
2026-06-09 14:58:25 -07:00
brain = new Brainy ( { requireSubtype : false ,
2025-11-18 16:06:34 -08:00
storage : {
type : 'filesystem' ,
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.
2026-06-20 13:31:11 -07:00
path : testDir
2025-11-18 16:06:34 -08:00
} ,
silent : true
} )
await brain . init ( )
vfs = new VirtualFileSystem ( brain )
await vfs . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
rmSync ( testDir , { recursive : true , force : true } )
} )
it ( 'VFS should automatically use metadata-only' , async ( ) = > {
await vfs . writeFile ( '/test.txt' , Buffer . from ( 'Test content' ) )
// VFS readFile internally uses brain.get() - should be metadata-only
const content = await vfs . readFile ( '/test.txt' )
expect ( content . toString ( ) ) . toBe ( 'Test content' )
} )
it ( 'VFS stat() should be fast with metadata-only' , async ( ) = > {
await vfs . writeFile ( '/test.txt' , Buffer . from ( 'Test content' ) )
const start = performance . now ( )
const stats = await vfs . stat ( '/test.txt' )
const time = performance . now ( ) - start
expect ( stats ) . toBeDefined ( )
expect ( stats . size ) . toBeGreaterThan ( 0 )
2026-06-17 13:11:41 -07:00
// PERF: env-dependent — relaxed generously from the original 50ms.
expect ( time ) . toBeLessThan ( 250 )
2025-11-18 16:06:34 -08:00
} )
it ( 'VFS readdir() should be fast with metadata-only' , async ( ) = > {
// Create 10 files
for ( let i = 0 ; i < 10 ; i ++ ) {
await vfs . writeFile ( ` /file ${ i } .txt ` , Buffer . from ( ` Content ${ i } ` ) )
}
const start = performance . now ( )
const files = await vfs . readdir ( '/' )
const time = performance . now ( ) - start
expect ( files . length ) . toBe ( 10 )
2026-06-17 13:11:41 -07:00
// PERF: env-dependent — relaxed generously from the original 200ms.
expect ( time ) . toBeLessThan ( 1000 )
2025-11-18 16:06:34 -08:00
} )
} )
describe ( 'Performance Verification' , ( ) = > {
it ( 'metadata-only should be significantly faster' , async ( ) = > {
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.
OPT-OUT REMAINS FULLY SUPPORTED
The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:
- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
contract entirely. Recommended only for migration windows or test
fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
optional vocabulary. Composes with the brain-wide flag.
Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.
TEST SWEEP
Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
- `new Brainy({` → `new Brainy({ requireSubtype: false,`
- `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
- `new Brainy()` → `new Brainy({ requireSubtype: false })`
tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.
The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.
CHANGES
src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
Comment refreshed to document the three opt-out paths.
tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
opt-out preserves the test author's original intent.
tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.
NO-OP for consumers who were already passing subtype on every write.
For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
no other regressions from the flip)
2026-06-09 14:58:25 -07:00
const brain = new Brainy ( { requireSubtype : false ,
2026-06-17 13:11:41 -07:00
storage : { type : 'memory' } ,
2025-11-18 16:06:34 -08:00
silent : true
} )
await brain . init ( )
const id = await brain . add ( {
data : 'Performance test' ,
type : NounType . Document
} )
// Warm up
await brain . get ( id )
await brain . get ( id , { includeVectors : true } )
// Measure metadata-only
const iterations = 50
const metadataStart = performance . now ( )
for ( let i = 0 ; i < iterations ; i ++ ) {
await brain . get ( id )
}
const metadataTime = ( performance . now ( ) - metadataStart ) / iterations
// Measure full entity
const fullStart = performance . now ( )
for ( let i = 0 ; i < iterations ; i ++ ) {
await brain . get ( id , { includeVectors : true } )
}
const fullTime = ( performance . now ( ) - fullStart ) / iterations
// Metadata-only should be faster
expect ( metadataTime ) . toBeLessThan ( fullTime )
const speedup = ( ( fullTime - metadataTime ) / fullTime ) * 100
console . log ( ` [Performance] Metadata-only: ${ metadataTime . toFixed ( 2 ) } ms, Full: ${ fullTime . toFixed ( 2 ) } ms, Speedup: ${ speedup . toFixed ( 1 ) } % ` )
await brain . close ( )
} )
} )
} )