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)
367 lines
No EOL
12 KiB
TypeScript
367 lines
No EOL
12 KiB
TypeScript
/**
|
|
* Brainy v3.0 Find & Triple Intelligence Test Suite
|
|
* Testing vector search, metadata filtering, and fusion strategies
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
import { Brainy } from '../../src/brainy.js'
|
|
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
|
|
|
describe('Find and Triple Intelligence', () => {
|
|
let brain: Brainy
|
|
let testData: any[]
|
|
|
|
beforeAll(async () => {
|
|
brain = new Brainy({ requireSubtype: false,
|
|
storage: { type: 'memory' }
|
|
})
|
|
await brain.init()
|
|
|
|
// Add diverse test data
|
|
testData = [
|
|
// Technology items
|
|
{ data: 'JavaScript is a programming language for web development', metadata: { category: 'tech', language: 'javascript', difficulty: 'medium' }, type: NounType.Document },
|
|
{ data: 'TypeScript adds static typing to JavaScript', metadata: { category: 'tech', language: 'typescript', difficulty: 'advanced' }, type: NounType.Document },
|
|
{ data: 'Python is great for data science and AI', metadata: { category: 'tech', language: 'python', difficulty: 'easy' }, type: NounType.Document },
|
|
{ data: 'React is a JavaScript library for building UIs', metadata: { category: 'tech', framework: 'react', language: 'javascript' }, type: NounType.Document },
|
|
|
|
// Science items
|
|
{ data: 'Quantum computing uses quantum mechanics principles', metadata: { category: 'science', field: 'physics', complexity: 'high' }, type: NounType.Concept },
|
|
{ data: 'Machine learning enables computers to learn from data', metadata: { category: 'science', field: 'ai', complexity: 'medium' }, type: NounType.Concept },
|
|
{ data: 'Neural networks are inspired by biological brains', metadata: { category: 'science', field: 'ai', complexity: 'high' }, type: NounType.Concept },
|
|
|
|
// Business items
|
|
{ data: 'Market analysis helps understand consumer behavior', metadata: { category: 'business', domain: 'marketing', importance: 'high' }, type: NounType.Process },
|
|
{ data: 'Project management ensures successful delivery', metadata: { category: 'business', domain: 'management', importance: 'critical' }, type: NounType.Process },
|
|
{ data: 'Financial planning is essential for growth', metadata: { category: 'business', domain: 'finance', importance: 'critical' }, type: NounType.Process }
|
|
]
|
|
|
|
// Add all test data
|
|
for (const item of testData) {
|
|
await brain.add(item)
|
|
}
|
|
})
|
|
|
|
afterAll(() => {
|
|
brain = null as any
|
|
})
|
|
|
|
describe('Vector Search', () => {
|
|
it('should find similar items by text query', async () => {
|
|
const results = await brain.find({
|
|
query: 'JavaScript programming',
|
|
limit: 3
|
|
})
|
|
|
|
expect(results).toHaveLength(3)
|
|
expect(results[0].score).toBeGreaterThan(0)
|
|
expect(results[0].score).toBeLessThanOrEqual(1)
|
|
|
|
// Should find JavaScript-related items first
|
|
const topResult = results[0].entity
|
|
expect(topResult.metadata?.language).toBeDefined()
|
|
})
|
|
|
|
it('should respect limit parameter', async () => {
|
|
const results = await brain.find({
|
|
query: 'technology',
|
|
limit: 5
|
|
})
|
|
|
|
expect(results.length).toBeLessThanOrEqual(5)
|
|
})
|
|
|
|
it('should find by vector directly', async () => {
|
|
// Get vector from an existing item
|
|
const jsItem = await brain.find({ query: 'JavaScript', limit: 1 })
|
|
const vector = jsItem[0].entity.vector
|
|
|
|
const results = await brain.find({
|
|
vector,
|
|
limit: 3
|
|
})
|
|
|
|
expect(results).toHaveLength(3)
|
|
// First result should be very similar (same or nearly same vector)
|
|
expect(results[0].score).toBeGreaterThan(0.9)
|
|
})
|
|
|
|
it('should return all items when no query provided', async () => {
|
|
const results = await brain.find({})
|
|
|
|
expect(results.length).toBeGreaterThanOrEqual(testData.length)
|
|
})
|
|
})
|
|
|
|
describe('Metadata Filtering', () => {
|
|
it('should filter by single metadata field', async () => {
|
|
const results = await brain.find({
|
|
where: { category: 'tech' }
|
|
})
|
|
|
|
expect(results.length).toBeGreaterThan(0)
|
|
results.forEach(r => {
|
|
expect(r.entity.metadata?.category).toBe('tech')
|
|
})
|
|
})
|
|
|
|
it('should filter by multiple metadata fields', async () => {
|
|
const results = await brain.find({
|
|
where: {
|
|
category: 'tech',
|
|
language: 'javascript'
|
|
}
|
|
})
|
|
|
|
results.forEach(r => {
|
|
expect(r.entity.metadata?.category).toBe('tech')
|
|
expect(r.entity.metadata?.language).toBe('javascript')
|
|
})
|
|
})
|
|
|
|
it('should support $gte operator', async () => {
|
|
const results = await brain.find({
|
|
where: {
|
|
importance: { $gte: 'high' }
|
|
}
|
|
})
|
|
|
|
results.forEach(r => {
|
|
const importance = r.entity.metadata?.importance
|
|
if (importance) {
|
|
expect(['high', 'critical']).toContain(importance)
|
|
}
|
|
})
|
|
})
|
|
|
|
it('should support $contains operator for arrays', async () => {
|
|
// Add item with array metadata
|
|
await brain.add({
|
|
data: 'Test with tags',
|
|
metadata: { tags: ['test', 'validation', 'qa'] },
|
|
type: NounType.Document
|
|
})
|
|
|
|
const results = await brain.find({
|
|
where: {
|
|
tags: { $contains: 'test' }
|
|
}
|
|
})
|
|
|
|
const found = results.find(r =>
|
|
Array.isArray(r.entity.metadata?.tags) &&
|
|
r.entity.metadata.tags.includes('test')
|
|
)
|
|
expect(found).toBeDefined()
|
|
})
|
|
|
|
it('should handle complex nested filters', async () => {
|
|
const results = await brain.find({
|
|
where: {
|
|
$or: [
|
|
{ category: 'tech', language: 'javascript' },
|
|
{ category: 'science', field: 'ai' }
|
|
]
|
|
}
|
|
})
|
|
|
|
results.forEach(r => {
|
|
const meta = r.entity.metadata
|
|
const matchesTech = meta?.category === 'tech' && meta?.language === 'javascript'
|
|
const matchesScience = meta?.category === 'science' && meta?.field === 'ai'
|
|
expect(matchesTech || matchesScience).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Type Filtering', () => {
|
|
it('should filter by single noun type', async () => {
|
|
const results = await brain.find({
|
|
type: NounType.Document
|
|
})
|
|
|
|
results.forEach(r => {
|
|
expect(r.entity.type).toBe(NounType.Document)
|
|
})
|
|
})
|
|
|
|
it('should filter by multiple noun types', async () => {
|
|
const results = await brain.find({
|
|
type: [NounType.Document, NounType.Concept]
|
|
})
|
|
|
|
results.forEach(r => {
|
|
expect([NounType.Document, NounType.Concept]).toContain(r.entity.type)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Combined Search (Triple Intelligence)', () => {
|
|
it('should combine vector search with metadata filtering', async () => {
|
|
const results = await brain.find({
|
|
query: 'programming',
|
|
where: { category: 'tech' },
|
|
limit: 5
|
|
})
|
|
|
|
expect(results.length).toBeGreaterThan(0)
|
|
results.forEach(r => {
|
|
expect(r.entity.metadata?.category).toBe('tech')
|
|
})
|
|
})
|
|
|
|
it('should combine vector, metadata, and type filtering', async () => {
|
|
const results = await brain.find({
|
|
query: 'artificial intelligence',
|
|
where: { category: 'science' },
|
|
type: NounType.Concept,
|
|
limit: 5
|
|
})
|
|
|
|
results.forEach(r => {
|
|
expect(r.entity.metadata?.category).toBe('science')
|
|
expect(r.entity.type).toBe(NounType.Concept)
|
|
})
|
|
})
|
|
|
|
it('should support fusion strategies', async () => {
|
|
const results = await brain.find({
|
|
query: 'JavaScript',
|
|
where: { category: 'tech' },
|
|
fusion: {
|
|
strategy: 'adaptive',
|
|
weights: { vector: 0.7, field: 0.3 }
|
|
}
|
|
})
|
|
|
|
expect(results).toBeDefined()
|
|
expect(results.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should handle fusion with graph intelligence', async () => {
|
|
// Create some relationships
|
|
const items = await brain.find({ limit: 3 })
|
|
if (items.length >= 2) {
|
|
await brain.relate({
|
|
from: items[0].id,
|
|
to: items[1].id,
|
|
type: VerbType.References
|
|
})
|
|
}
|
|
|
|
const results = await brain.find({
|
|
query: 'technology',
|
|
connected: { to: items[0]?.id },
|
|
fusion: {
|
|
strategy: 'adaptive',
|
|
weights: { vector: 0.5, graph: 0.3, field: 0.2 }
|
|
}
|
|
})
|
|
|
|
expect(results).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('Performance', () => {
|
|
it('should handle large result sets efficiently', async () => {
|
|
const start = Date.now()
|
|
const results = await brain.find({ limit: 100 })
|
|
const elapsed = Date.now() - start
|
|
|
|
expect(elapsed).toBeLessThan(1000) // Should complete in under 1 second
|
|
expect(results.length).toBeLessThanOrEqual(100)
|
|
})
|
|
|
|
it('should cache repeated searches', async () => {
|
|
const query = { query: 'caching test', limit: 5 }
|
|
|
|
// First search
|
|
const results1 = await brain.find(query)
|
|
|
|
// Second search (should hit cache)
|
|
const results2 = await brain.find(query)
|
|
|
|
expect(results1).toEqual(results2)
|
|
// Note: Cache might not always be faster in tests due to overhead
|
|
// But results should be identical
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty query gracefully', async () => {
|
|
const results = await brain.find({ query: '' })
|
|
expect(results).toBeDefined()
|
|
expect(Array.isArray(results)).toBe(true)
|
|
})
|
|
|
|
it('should handle non-matching filters', async () => {
|
|
const results = await brain.find({
|
|
where: { nonExistentField: 'value' }
|
|
})
|
|
|
|
expect(results).toBeDefined()
|
|
expect(Array.isArray(results)).toBe(true)
|
|
})
|
|
|
|
it('should handle very long queries', async () => {
|
|
const longQuery = 'test '.repeat(1000) // 5000 characters
|
|
const results = await brain.find({ query: longQuery, limit: 1 })
|
|
|
|
expect(results).toBeDefined()
|
|
expect(Array.isArray(results)).toBe(true)
|
|
})
|
|
|
|
it('should handle special characters in queries', async () => {
|
|
const specialQuery = '!@#$%^&*()_+-=[]{}|;\':",./<>?'
|
|
const results = await brain.find({ query: specialQuery, limit: 1 })
|
|
|
|
expect(results).toBeDefined()
|
|
expect(Array.isArray(results)).toBe(true)
|
|
})
|
|
|
|
it('should handle unicode in queries', async () => {
|
|
const unicodeQuery = '你好世界 🌍 مرحبا بالعالم'
|
|
const results = await brain.find({ query: unicodeQuery, limit: 1 })
|
|
|
|
expect(results).toBeDefined()
|
|
expect(Array.isArray(results)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Similarity Search', () => {
|
|
it('should find similar items to a given ID', async () => {
|
|
const items = await brain.find({ limit: 1 })
|
|
if (items.length > 0) {
|
|
const results = await brain.similar({
|
|
to: items[0].id,
|
|
limit: 3
|
|
})
|
|
|
|
expect(results).toHaveLength(3)
|
|
// First result might be the item itself
|
|
results.forEach(r => {
|
|
expect(r.score).toBeGreaterThanOrEqual(0)
|
|
expect(r.score).toBeLessThanOrEqual(1)
|
|
})
|
|
}
|
|
})
|
|
|
|
it('should exclude the source item from similar results', async () => {
|
|
const items = await brain.find({ limit: 1 })
|
|
if (items.length > 0) {
|
|
const results = await brain.similar({
|
|
to: items[0].id,
|
|
limit: 5
|
|
})
|
|
|
|
// Check if source is excluded (implementation dependent)
|
|
const selfMatch = results.find(r => r.id === items[0].id)
|
|
if (selfMatch) {
|
|
// If included, should be first with score ~1
|
|
expect(results[0].id).toBe(items[0].id)
|
|
expect(results[0].score).toBeGreaterThan(0.99)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}) |