2025-09-11 16:23:32 -07:00
/ * *
* Comprehensive Public API Test Suite
2026-02-17 17:04:11 -08:00
*
* Validates all public API methods exposed by Brainy ,
2025-09-11 16:23:32 -07:00
* ensuring complete coverage of documented functionality .
* /
import { describe , it , expect , beforeAll , afterAll , beforeEach , afterEach , vi } from 'vitest'
import { Brainy } from '../../src/brainy'
import { NounType , VerbType } from '../../src/types/graphTypes'
2026-02-17 17:04:11 -08:00
import type { Entity , Result } from '../../src/types/brainy.types'
2025-09-11 16:23:32 -07:00
import * as fs from 'fs/promises'
import * as path from 'path'
import { tmpdir } from 'os'
2026-02-17 17:04:11 -08:00
import { randomUUID } from 'crypto'
2025-09-11 16:23:32 -07:00
describe ( 'Brainy Public API - Complete Coverage' , ( ) = > {
let brain : Brainy
let testDir : string
beforeAll ( async ( ) = > {
testDir = path . join ( tmpdir ( ) , ` brainy-test- ${ Date . now ( ) } ` )
await fs . mkdir ( testDir , { recursive : true } )
} )
afterAll ( async ( ) = > {
try {
await fs . rm ( testDir , { recursive : true , force : true } )
} catch ( error ) {
console . warn ( 'Failed to cleanup test directory:' , error )
}
} )
describe ( 'Core CRUD Operations' , ( ) = > {
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 , storage : { type : 'memory' } } )
2025-09-11 16:23:32 -07:00
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
describe ( 'brain.add()' , ( ) = > {
it ( 'should handle all noun types' , async ( ) = > {
const nounTypes = Object . values ( NounType )
2026-02-17 17:04:11 -08:00
2025-09-11 16:23:32 -07:00
for ( const nounType of nounTypes ) {
const id = await brain . add ( {
data : ` Test ${ nounType } ` ,
type : nounType ,
metadata : { nounType }
} )
2026-02-17 17:04:11 -08:00
2025-09-11 16:23:32 -07:00
expect ( id ) . toBeDefined ( )
expect ( typeof id ) . toBe ( 'string' )
2026-02-17 17:04:11 -08:00
2025-09-11 16:23:32 -07:00
const entity = await brain . get ( id )
expect ( entity ) . toBeDefined ( )
expect ( entity ? . type ) . toBe ( nounType )
}
2026-02-17 17:04:11 -08:00
} , 120000 )
2025-09-11 16:23:32 -07:00
it ( 'should validate required fields' , async ( ) = > {
await expect ( brain . add ( {
type : NounType . Document
} as any ) ) . rejects . toThrow ( )
await expect ( brain . add ( {
data : 'test'
} as any ) ) . rejects . toThrow ( )
} )
it ( 'should handle very large data' , async ( ) = > {
2026-02-17 17:04:11 -08:00
const largeText = 'x' . repeat ( 1 _000_000 )
2025-09-11 16:23:32 -07:00
const id = await brain . add ( {
data : largeText ,
type : NounType . Document
} )
2026-02-17 17:04:11 -08:00
2025-09-11 16:23:32 -07:00
const entity = await brain . get ( id )
2026-02-17 17:04:11 -08:00
expect ( entity ? . data ) . toBe ( largeText )
2025-09-11 16:23:32 -07:00
} )
} )
describe ( 'brain.addMany()' , ( ) = > {
2026-02-17 17:04:11 -08:00
it ( 'should handle batch operations' , async ( ) = > {
const items = Array . from ( { length : 10 } , ( _ , i ) = > ( {
data : ` Batch item ${ i } ` ,
2025-09-11 16:23:32 -07:00
type : NounType . Document ,
metadata : { index : i }
} ) )
2026-02-17 17:04:11 -08:00
const result = await brain . addMany ( { items } )
expect ( result . successful ) . toHaveLength ( 10 )
} , 120000 )
2025-09-11 16:23:32 -07:00
it ( 'should handle partial failures' , async ( ) = > {
const items = [
{ data : 'Valid 1' , type : NounType . Document } ,
2026-02-17 17:04:11 -08:00
{ data : null as any , type : NounType . Document } ,
2025-09-11 16:23:32 -07:00
{ data : 'Valid 2' , type : NounType . Document }
]
try {
2026-02-17 17:04:11 -08:00
const result = await brain . addMany ( { items } )
expect ( result . successful . length ) . toBeGreaterThan ( 0 )
2025-09-11 16:23:32 -07:00
} catch ( error ) {
expect ( error ) . toBeDefined ( )
}
} )
} )
describe ( 'brain.update()' , ( ) = > {
it ( 'should handle concurrent updates' , async ( ) = > {
const id = await brain . add ( {
data : 'Initial' ,
type : NounType . Document ,
metadata : { version : 1 }
} )
2026-02-17 17:04:11 -08:00
const updates = Array . from ( { length : 10 } , ( _ , i ) = >
brain . update ( {
id ,
2025-09-11 16:23:32 -07:00
metadata : { version : i + 2 , updatedBy : ` thread- ${ i } ` }
} )
)
await Promise . all ( updates )
const final = await brain . get ( id )
expect ( final ) . toBeDefined ( )
expect ( final ? . metadata ? . version ) . toBeGreaterThan ( 1 )
} )
2026-06-11 14:51:00 -07:00
it ( 'should reject update of a non-existent entity' , async ( ) = > {
2026-02-17 17:04:11 -08:00
const fakeId = randomUUID ( )
2026-06-11 14:51:00 -07:00
await expect (
brain . update ( { id : fakeId , metadata : { test : true } } )
) . rejects . toThrow ( /not found/ )
2025-09-11 16:23:32 -07:00
} )
it ( 'should preserve unmodified fields' , async ( ) = > {
const id = await brain . add ( {
data : 'Test' ,
type : NounType . Document ,
2026-02-17 17:04:11 -08:00
metadata : {
2025-09-11 16:23:32 -07:00
field1 : 'value1' ,
field2 : 'value2' ,
nested : { a : 1 , b : 2 }
}
} )
2026-02-17 17:04:11 -08:00
await brain . update ( {
id ,
2025-09-11 16:23:32 -07:00
metadata : { field1 : 'updated' }
} )
const updated = await brain . get ( id )
expect ( updated ? . metadata ? . field1 ) . toBe ( 'updated' )
expect ( updated ? . metadata ? . field2 ) . toBe ( 'value2' )
expect ( updated ? . metadata ? . nested ) . toEqual ( { a : 1 , b : 2 } )
} )
} )
describe ( 'brain.updateMany()' , ( ) = > {
it ( 'should batch update multiple entities' , async ( ) = > {
2026-02-17 17:04:11 -08:00
const result = await brain . addMany ( {
items : [
{ data : 'Item 1' , type : NounType . Document } ,
{ data : 'Item 2' , type : NounType . Document } ,
{ data : 'Item 3' , type : NounType . Document }
]
} )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
const ids = result . successful
2025-09-11 16:23:32 -07:00
const updates = ids . map ( id = > ( {
id ,
2026-02-17 17:04:11 -08:00
metadata : { updated : true }
2025-09-11 16:23:32 -07:00
} ) )
2026-02-17 17:04:11 -08:00
const updateResult = await brain . updateMany ( { items : updates } )
expect ( updateResult ) . toBeDefined ( )
2025-09-11 16:23:32 -07:00
for ( const id of ids ) {
const entity = await brain . get ( id )
expect ( entity ? . metadata ? . updated ) . toBe ( true )
}
} )
} )
2026-06-11 14:51:00 -07:00
describe ( 'brain.remove()' , ( ) = > {
2025-09-11 16:23:32 -07:00
it ( 'should handle cascade deletion of relationships' , async ( ) = > {
const id1 = await brain . add ( { data : 'Entity 1' , type : NounType . Person } )
const id2 = await brain . add ( { data : 'Entity 2' , type : NounType . Organization } )
2026-02-17 17:04:11 -08:00
await brain . relate ( { from : id1 , to : id2 , type : VerbType . WorksWith } )
2026-06-11 14:51:00 -07:00
await brain . remove ( id1 )
2026-02-17 17:04:11 -08:00
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( id2 )
2026-02-17 17:04:11 -08:00
expect ( relations . filter ( r = > r . from === id1 || r . to === id1 ) ) . toHaveLength ( 0 )
2025-09-11 16:23:32 -07:00
} )
2026-06-11 14:51:00 -07:00
it ( 'should treat deletion of a non-existent entity as a no-op' , async ( ) = > {
2026-02-17 17:04:11 -08:00
const fakeId = randomUUID ( )
2026-06-11 14:51:00 -07:00
// delete() is idempotent: unknown IDs resolve without throwing
await expect ( brain . remove ( fakeId ) ) . resolves . toBeUndefined ( )
2025-09-11 16:23:32 -07:00
} )
} )
2026-06-11 14:51:00 -07:00
describe ( 'brain.removeMany()' , ( ) = > {
2026-02-17 17:04:11 -08:00
it ( 'should efficiently delete batches' , async ( ) = > {
const result = await brain . addMany ( {
items : Array.from ( { length : 5 } , ( _ , i ) = > ( {
2025-09-11 16:23:32 -07:00
data : ` Item ${ i } ` ,
type : NounType . Document
} ) )
2026-02-17 17:04:11 -08:00
} )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
const ids = result . successful
2026-06-11 14:51:00 -07:00
await brain . removeMany ( { ids } )
2025-09-11 16:23:32 -07:00
for ( const id of ids ) {
const entity = await brain . get ( id )
expect ( entity ) . toBeNull ( )
}
} )
} )
} )
describe ( 'Relationship Operations' , ( ) = > {
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 , storage : { type : 'memory' } } )
2025-09-11 16:23:32 -07:00
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
describe ( 'brain.relate()' , ( ) = > {
2026-02-17 17:04:11 -08:00
it ( 'should create relationships with multiple verb types' , async ( ) = > {
2025-09-11 16:23:32 -07:00
const id1 = await brain . add ( { data : 'Source' , type : NounType . Person } )
const id2 = await brain . add ( { data : 'Target' , type : NounType . Organization } )
2026-02-17 17:04:11 -08:00
// Test a representative subset of verb types (not all 127)
const testVerbs = [
VerbType . RelatedTo , VerbType . Creates , VerbType . References ,
VerbType . WorksWith , VerbType . DependsOn , VerbType . Contains ,
VerbType . Requires , VerbType . FriendOf
]
for ( const verbType of testVerbs ) {
const relationId = await brain . relate ( {
from : id1 ,
to : id2 ,
type : verbType ,
2025-09-11 16:23:32 -07:00
metadata : { verbType }
} )
2026-02-17 17:04:11 -08:00
2025-09-11 16:23:32 -07:00
expect ( relationId ) . toBeDefined ( )
expect ( typeof relationId ) . toBe ( 'string' )
}
} )
it ( 'should handle bidirectional relationships' , async ( ) = > {
const person1 = await brain . add ( { data : 'Alice' , type : NounType . Person } )
const person2 = await brain . add ( { data : 'Bob' , type : NounType . Person } )
2026-02-17 17:04:11 -08:00
await brain . relate ( {
from : person1 ,
to : person2 ,
type : VerbType . FriendOf ,
2025-09-11 16:23:32 -07:00
bidirectional : true
} )
2026-06-11 14:51:00 -07:00
const relations1 = await brain . related ( person1 )
const relations2 = await brain . related ( person2 )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
expect ( relations1 . some ( r = > r . to === person2 ) ) . toBe ( true )
expect ( relations2 . some ( r = > r . to === person1 ) ) . toBe ( true )
2025-09-11 16:23:32 -07:00
} )
2026-02-17 17:04:11 -08:00
it ( 'should allow duplicate relationships' , async ( ) = > {
2025-09-11 16:23:32 -07:00
const id1 = await brain . add ( { data : 'A' , type : NounType . Document } )
const id2 = await brain . add ( { data : 'B' , type : NounType . Document } )
2026-02-17 17:04:11 -08:00
await brain . relate ( { from : id1 , to : id2 , type : VerbType . References } )
await brain . relate ( { from : id1 , to : id2 , type : VerbType . References } )
2025-09-11 16:23:32 -07:00
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( id1 )
2025-09-11 16:23:32 -07:00
const referenceRelations = relations . filter (
2026-02-17 17:04:11 -08:00
r = > r . type === VerbType . References && r . to === id2
2025-09-11 16:23:32 -07:00
)
2026-02-17 17:04:11 -08:00
expect ( referenceRelations . length ) . toBeGreaterThanOrEqual ( 1 )
2025-09-11 16:23:32 -07:00
} )
it ( 'should handle relationship metadata and weights' , async ( ) = > {
const id1 = await brain . add ( { data : 'Source' , type : NounType . Document } )
const id2 = await brain . add ( { data : 'Target' , type : NounType . Document } )
2026-02-17 17:04:11 -08:00
const relationId = await brain . relate ( {
from : id1 ,
to : id2 ,
type : VerbType . References ,
2025-09-11 16:23:32 -07:00
weight : 0.8 ,
metadata : {
context : 'academic' ,
verified : true
}
} )
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( id1 )
2025-09-11 16:23:32 -07:00
const relation = relations . find ( r = > r . id === relationId )
2026-02-17 17:04:11 -08:00
expect ( relation ) . toBeDefined ( )
expect ( relation ? . metadata ? . context ) . toBe ( 'academic' )
2025-09-11 16:23:32 -07:00
} )
} )
describe ( 'brain.relateMany()' , ( ) = > {
it ( 'should create multiple relationships efficiently' , async ( ) = > {
2026-02-17 17:04:11 -08:00
const result = await brain . addMany ( {
items : Array.from ( { length : 5 } , ( _ , i ) = > ( {
2025-09-11 16:23:32 -07:00
data : ` Entity ${ i } ` ,
type : NounType . Document
} ) )
2026-02-17 17:04:11 -08:00
} )
const entities = result . successful
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
const items = [ ]
2025-09-11 16:23:32 -07:00
for ( let i = 0 ; i < entities . length - 1 ; i ++ ) {
2026-02-17 17:04:11 -08:00
items . push ( {
from : entities [ i ] ,
to : entities [ i + 1 ] ,
type : VerbType . Precedes
2025-09-11 16:23:32 -07:00
} )
}
2026-02-17 17:04:11 -08:00
const relationIds = await brain . relateMany ( { items } )
expect ( relationIds ) . toHaveLength ( items . length )
2025-09-11 16:23:32 -07:00
} )
} )
2026-06-11 14:51:00 -07:00
describe ( 'brain.related()' , ( ) = > {
2026-02-17 17:04:11 -08:00
it ( 'should retrieve outgoing relationships' , async ( ) = > {
2025-09-11 16:23:32 -07:00
const center = await brain . add ( { data : 'Center' , type : NounType . Person } )
const related1 = await brain . add ( { data : 'Related1' , type : NounType . Organization } )
const related2 = await brain . add ( { data : 'Related2' , type : NounType . Document } )
2026-02-17 17:04:11 -08:00
await brain . relate ( { from : center , to : related1 , type : VerbType . WorksWith } )
await brain . relate ( { from : center , to : related2 , type : VerbType . Creates } )
2026-06-11 14:51:00 -07:00
const outgoing = await brain . related ( { from : center } )
2026-02-17 17:04:11 -08:00
expect ( outgoing ) . toHaveLength ( 2 )
expect ( outgoing . some ( r = > r . type === VerbType . WorksWith ) ) . toBe ( true )
expect ( outgoing . some ( r = > r . type === VerbType . Creates ) ) . toBe ( true )
} )
it ( 'should retrieve incoming relationships' , async ( ) = > {
const center = await brain . add ( { data : 'Center' , type : NounType . Person } )
const source = await brain . add ( { data : 'Source' , type : NounType . Task } )
await brain . relate ( { from : source , to : center , type : VerbType . DependsOn } )
2026-06-11 14:51:00 -07:00
const incoming = await brain . related ( { to : center } )
2026-02-17 17:04:11 -08:00
expect ( incoming ) . toHaveLength ( 1 )
expect ( incoming [ 0 ] . type ) . toBe ( VerbType . DependsOn )
2025-09-11 16:23:32 -07:00
} )
it ( 'should filter by relationship direction' , async ( ) = > {
const center = await brain . add ( { data : 'Center' , type : NounType . Document } )
const source = await brain . add ( { data : 'Source' , type : NounType . Person } )
const target = await brain . add ( { data : 'Target' , type : NounType . Task } )
2026-02-17 17:04:11 -08:00
await brain . relate ( { from : source , to : center , type : VerbType . Creates } )
await brain . relate ( { from : center , to : target , type : VerbType . Requires } )
2025-09-11 16:23:32 -07:00
2026-06-11 14:51:00 -07:00
const outgoing = await brain . related ( { from : center } )
const incoming = await brain . related ( { to : center } )
2025-09-11 16:23:32 -07:00
expect ( outgoing ) . toHaveLength ( 1 )
2026-02-17 17:04:11 -08:00
expect ( outgoing [ 0 ] . to ) . toBe ( target )
2025-09-11 16:23:32 -07:00
expect ( incoming ) . toHaveLength ( 1 )
2026-02-17 17:04:11 -08:00
expect ( incoming [ 0 ] . from ) . toBe ( source )
2025-09-11 16:23:32 -07:00
} )
it ( 'should filter by verb type' , async ( ) = > {
const doc = await brain . add ( { data : 'Document' , type : NounType . Document } )
const ref1 = await brain . add ( { data : 'Reference1' , type : NounType . Document } )
const ref2 = await brain . add ( { data : 'Reference2' , type : NounType . Document } )
const author = await brain . add ( { data : 'Author' , type : NounType . Person } )
2026-02-17 17:04:11 -08:00
await brain . relate ( { from : doc , to : ref1 , type : VerbType . References } )
await brain . relate ( { from : doc , to : ref2 , type : VerbType . References } )
await brain . relate ( { from : author , to : doc , type : VerbType . Creates } )
2025-09-11 16:23:32 -07:00
2026-06-11 14:51:00 -07:00
const references = await brain . related ( {
2026-02-17 17:04:11 -08:00
from : doc ,
type : VerbType . References
2025-09-11 16:23:32 -07:00
} )
expect ( references ) . toHaveLength ( 2 )
2026-02-17 17:04:11 -08:00
expect ( references . every ( r = > r . type === VerbType . References ) ) . toBe ( true )
2025-09-11 16:23:32 -07:00
} )
} )
} )
describe ( 'Search Operations' , ( ) = > {
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 , storage : { type : 'memory' } } )
2025-09-11 16:23:32 -07:00
await brain . init ( )
2026-02-17 17:04:11 -08:00
await brain . addMany ( {
items : [
{ data : 'The quick brown fox' , type : NounType . Document , metadata : { category : 'animals' } } ,
{ data : 'jumps over the lazy dog' , type : NounType . Document , metadata : { category : 'animals' } } ,
{ data : 'Machine learning algorithms' , type : NounType . Document , metadata : { category : 'tech' } } ,
{ data : 'Deep neural networks' , type : NounType . Document , metadata : { category : 'tech' } } ,
{ data : 'Natural language processing' , type : NounType . Document , metadata : { category : 'tech' } }
]
} )
} , 120000 )
2025-09-11 16:23:32 -07:00
afterEach ( async ( ) = > {
await brain . close ( )
} )
describe ( 'brain.find()' , ( ) = > {
2026-02-17 17:04:11 -08:00
it ( 'should support metadata search' , async ( ) = > {
const results = await brain . find ( {
where : { category : 'tech' } ,
limit : 5
} )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
expect ( results ) . toBeDefined ( )
expect ( Array . isArray ( results ) ) . toBe ( true )
expect ( results . length ) . toBeGreaterThan ( 0 )
2025-09-11 16:23:32 -07:00
} )
2026-02-17 17:04:11 -08:00
it ( 'should handle metadata filters' , async ( ) = > {
2025-09-11 16:23:32 -07:00
const results = await brain . find ( {
2026-02-17 17:04:11 -08:00
where : { category : 'tech' } ,
2025-09-11 16:23:32 -07:00
limit : 10
} )
expect ( results . length ) . toBeGreaterThan ( 0 )
expect ( results . every ( r = > r . entity . metadata ? . category === 'tech' ) ) . toBe ( true )
} )
it ( 'should support graph-connected searches' , async ( ) = > {
const doc1 = await brain . add ( { data : 'Primary document' , type : NounType . Document } )
const doc2 = await brain . add ( { data : 'Related document' , type : NounType . Document } )
2026-02-17 17:04:11 -08:00
await brain . relate ( { from : doc1 , to : doc2 , type : VerbType . References } )
2025-09-11 16:23:32 -07:00
const results = await brain . find ( {
query : 'document' ,
connected : { from : doc1 } ,
limit : 5
} )
expect ( results . some ( r = > r . entity . id === doc2 ) ) . toBe ( true )
} )
it ( 'should support pagination' , async ( ) = > {
const page1 = await brain . find ( {
2026-02-17 17:04:11 -08:00
where : { category : 'tech' } ,
2025-09-11 16:23:32 -07:00
limit : 2 ,
offset : 0
} )
const page2 = await brain . find ( {
2026-02-17 17:04:11 -08:00
where : { category : 'tech' } ,
2025-09-11 16:23:32 -07:00
limit : 2 ,
offset : 2
} )
2026-02-17 17:04:11 -08:00
if ( page1 . length > 0 && page2 . length > 0 ) {
expect ( page1 [ 0 ] ? . entity . id ) . not . toBe ( page2 [ 0 ] ? . entity . id )
}
2025-09-11 16:23:32 -07:00
} )
} )
describe ( 'brain.similar()' , ( ) = > {
it ( 'should find similar entities' , async ( ) = > {
const reference = await brain . add ( {
data : 'Artificial intelligence and machine learning' ,
type : NounType . Document
} )
2026-02-17 17:04:11 -08:00
const similar = await brain . similar ( {
to : reference ,
limit : 5
2025-09-11 16:23:32 -07:00
} )
expect ( similar ) . toBeDefined ( )
expect ( similar . length ) . toBeGreaterThan ( 0 )
} )
2026-02-17 17:04:11 -08:00
it ( 'should return results sorted by score' , async ( ) = > {
2025-09-11 16:23:32 -07:00
const reference = await brain . add ( {
2026-02-17 17:04:11 -08:00
data : 'Deep learning and neural networks research' ,
2025-09-11 16:23:32 -07:00
type : NounType . Document
} )
2026-02-17 17:04:11 -08:00
const similar = await brain . similar ( {
to : reference ,
limit : 10
2025-09-11 16:23:32 -07:00
} )
2026-02-17 17:04:11 -08:00
// Results should be sorted by score (descending)
if ( similar . length > 1 ) {
for ( let i = 1 ; i < similar . length ; i ++ ) {
expect ( similar [ i - 1 ] . score ) . toBeGreaterThanOrEqual ( similar [ i ] . score - 0.001 )
}
}
2025-09-11 16:23:32 -07:00
} )
} )
} )
2026-02-17 17:04:11 -08:00
describe ( 'Statistics' , ( ) = > {
2025-09-11 16:23:32 -07:00
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 , storage : { type : 'memory' } } )
2025-09-11 16:23:32 -07:00
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
2025-10-09 11:40:31 -07:00
describe ( 'brain.getStats()' , ( ) = > {
2026-02-17 17:04:11 -08:00
it ( 'should return statistics' , async ( ) = > {
await brain . addMany ( {
items : [
{ data : 'Item 1' , type : NounType . Document } ,
{ data : 'Item 2' , type : NounType . Person } ,
{ data : 'Item 3' , type : NounType . Task }
]
} )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
const stats = await brain . getStats ( )
2025-09-11 16:23:32 -07:00
expect ( stats ) . toBeDefined ( )
2026-02-17 17:04:11 -08:00
// Stats returns { entities: { total, byType }, relationships, density }
expect ( stats . entities ) . toBeDefined ( )
expect ( stats . entities . total ) . toBeGreaterThanOrEqual ( 3 )
expect ( stats . entities . byType ) . toBeDefined ( )
2025-09-11 16:23:32 -07:00
} )
} )
} )
describe ( 'FileSystem Storage' , ( ) = > {
it ( 'should handle basic CRUD with filesystem' , async ( ) = > {
2026-02-17 17:04:11 -08:00
const fsTestDir = path . join ( tmpdir ( ) , ` brainy-fs-crud- ${ Date . now ( ) } ` )
await fs . mkdir ( fsTestDir , { recursive : true } )
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 fsBrain = new Brainy ( { requireSubtype : false ,
2025-09-11 16:23:32 -07: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 : fsTestDir
2025-09-11 16:23:32 -07:00
}
} )
2026-02-17 17:04:11 -08:00
2025-09-11 16:23:32 -07:00
await fsBrain . init ( )
const id = await fsBrain . add ( {
data : 'Filesystem test' ,
type : NounType . Document
} )
const retrieved = await fsBrain . get ( id )
2026-02-17 17:04:11 -08:00
expect ( retrieved ? . data ) . toBe ( 'Filesystem test' )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
await fsBrain . update ( { id , metadata : { updated : true } } )
2025-09-11 16:23:32 -07:00
const updated = await fsBrain . get ( id )
expect ( updated ? . metadata ? . updated ) . toBe ( true )
2026-06-11 14:51:00 -07:00
await fsBrain . remove ( id )
2025-09-11 16:23:32 -07:00
const deleted = await fsBrain . get ( id )
expect ( deleted ) . toBeNull ( )
await fsBrain . close ( )
2026-02-17 17:04:11 -08:00
await fs . rm ( fsTestDir , { recursive : true , force : true } ) . catch ( ( ) = > { } )
2025-09-11 16:23:32 -07:00
} )
it ( 'should handle concurrent filesystem operations' , async ( ) = > {
2026-02-17 17:04:11 -08:00
const fsTestDir = path . join ( tmpdir ( ) , ` brainy-fs-concurrent- ${ Date . now ( ) } ` )
await fs . mkdir ( fsTestDir , { recursive : true } )
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 fsBrain = new Brainy ( { requireSubtype : false ,
2025-09-11 16:23:32 -07: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 : fsTestDir
2025-09-11 16:23:32 -07:00
}
} )
2026-02-17 17:04:11 -08:00
2025-09-11 16:23:32 -07:00
await fsBrain . init ( )
2026-02-17 17:04:11 -08:00
const operations = Array . from ( { length : 5 } , async ( _ , i ) = > {
2025-09-11 16:23:32 -07:00
const id = await fsBrain . add ( {
data : ` Concurrent ${ i } ` ,
type : NounType . Document
} )
return id
} )
const ids = await Promise . all ( operations )
2026-02-17 17:04:11 -08:00
expect ( ids ) . toHaveLength ( 5 )
expect ( new Set ( ids ) . size ) . toBe ( 5 )
2025-09-11 16:23:32 -07:00
await fsBrain . close ( )
2026-02-17 17:04:11 -08:00
await fs . rm ( fsTestDir , { recursive : true , force : true } ) . catch ( ( ) = > { } )
2025-09-11 16:23:32 -07:00
} )
} )
describe ( 'Error Recovery and Resilience' , ( ) = > {
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 , storage : { type : 'memory' } } )
2025-09-11 16:23:32 -07:00
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
2026-02-17 17:04:11 -08:00
it ( 'should handle and recover from transient errors' , async ( ) = > {
2025-09-11 16:23:32 -07:00
const originalAdd = brain . add . bind ( brain )
let failCount = 0
brain . add = vi . fn ( async ( . . . args ) = > {
if ( failCount ++ < 2 ) {
throw new Error ( 'Storage temporarily unavailable' )
}
return originalAdd ( . . . args )
} )
let succeeded = false
for ( let i = 0 ; i < 3 ; i ++ ) {
try {
await brain . add ( {
data : 'Test' ,
type : NounType . Document
} )
succeeded = true
break
} catch ( error ) {
// Expected for first attempts
}
}
expect ( succeeded ) . toBe ( true )
} )
it ( 'should handle malformed input gracefully' , async ( ) = > {
const malformedInputs = [
null ,
undefined ,
{ } ,
{ data : null } ,
{ type : 'invalid' } ,
]
for ( const input of malformedInputs ) {
try {
await brain . add ( input as any )
} catch ( error ) {
expect ( error ) . toBeDefined ( )
expect ( ( error as Error ) . message ) . toBeDefined ( )
}
}
} )
} )
2026-02-17 17:04:11 -08:00
describe ( 'Performance' , ( ) = > {
it ( 'should handle moderate-scale operations' , 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 perfBrain = new Brainy ( { requireSubtype : false , storage : { type : 'memory' } } )
2026-02-17 17:04:11 -08:00
await perfBrain . init ( )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
// Add 50 items in batches
const items = Array . from ( { length : 50 } , ( _ , i ) = > ( {
data : ` Performance item ${ i } ` ,
type : NounType . Document ,
metadata : { batch : Math.floor ( i / 10 ) , index : i }
} ) )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
const result = await perfBrain . addMany ( { items } )
expect ( result . successful . length ) . toBe ( 50 )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
// Search should work
const results = await perfBrain . find ( {
query : 'Performance item' ,
limit : 20
2025-09-11 16:23:32 -07:00
} )
expect ( results . length ) . toBeGreaterThan ( 0 )
2026-02-17 17:04:11 -08:00
await perfBrain . close ( )
} , 180000 )
2025-09-11 16:23:32 -07:00
} )
describe ( 'Clear Operations' , ( ) = > {
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 , storage : { type : 'memory' } } )
2025-09-11 16:23:32 -07:00
await brain . init ( )
2026-02-17 17:04:11 -08:00
await brain . addMany ( {
items : [
{ data : 'Doc 1' , type : NounType . Document , metadata : { category : 'A' } } ,
{ data : 'Doc 2' , type : NounType . Document , metadata : { category : 'B' } } ,
{ data : 'Person 1' , type : NounType . Person , metadata : { category : 'A' } }
]
} )
} , 120000 )
2025-09-11 16:23:32 -07:00
afterEach ( async ( ) = > {
await brain . close ( )
} )
2026-02-17 17:04:11 -08:00
it ( 'should clear data and allow re-use' , async ( ) = > {
const beforeClear = await brain . find ( { where : { category : 'A' } } )
expect ( beforeClear . length ) . toBeGreaterThan ( 0 )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
await brain . clear ( )
2025-09-11 16:23:32 -07:00
2026-02-17 17:04:11 -08:00
// After clear, add should still work
const id = await brain . add ( { data : 'After clear' , type : NounType . Document } )
const entity = await brain . get ( id )
expect ( entity ) . toBeDefined ( )
expect ( entity ? . data ) . toBe ( 'After clear' )
} , 120000 )
2025-09-11 16:23:32 -07:00
} )
2026-02-17 17:04:11 -08:00
} )