feat: implement comprehensive type safety system with BrainyTypes API
Major enhancements for type safety and developer experience: - Add BrainyTypes static API for type management and AI-powered suggestions - Implement strict type validation for all 31 NounType categories - Remove dangerous generic add() method that bypassed type safety - Add intelligent type inference with confidence scoring - Provide helpful error messages with typo suggestions using Levenshtein distance - Update all internal code, examples, and documentation to use typed methods - Enhance CLI with new type management commands (types, suggest, validate) Breaking changes: - Remove deprecated add() method - use addNoun() with explicit type parameter - All addNoun() calls now require explicit type as second parameter This release significantly improves type safety across the entire system while maintaining backward compatibility for properly typed method calls. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7e0c111d3c
commit
0f4ab52ad9
40 changed files with 1704 additions and 497 deletions
20
README.md
20
README.md
|
|
@ -44,13 +44,13 @@ const brain = new BrainyData()
|
|||
await brain.init()
|
||||
|
||||
// Add entities (nouns) with automatic embedding
|
||||
const jsId = await brain.addNoun("JavaScript is a programming language", {
|
||||
const jsId = await brain.addNoun("JavaScript is a programming language", 'concept', {
|
||||
type: "language",
|
||||
year: 1995,
|
||||
paradigm: "multi-paradigm"
|
||||
})
|
||||
|
||||
const nodeId = await brain.addNoun("Node.js runtime environment", {
|
||||
const nodeId = await brain.addNoun("Node.js runtime environment", 'concept', {
|
||||
type: "runtime",
|
||||
year: 2009,
|
||||
platform: "server-side"
|
||||
|
|
@ -225,7 +225,7 @@ const results = await brain.find({
|
|||
### CRUD Operations
|
||||
```javascript
|
||||
// Create entities (nouns)
|
||||
const id = await brain.addNoun(data, metadata)
|
||||
const id = await brain.addNoun(data, nounType, metadata)
|
||||
|
||||
// Create relationships (verbs)
|
||||
const verbId = await brain.addVerb(sourceId, targetId, "relationType", {
|
||||
|
|
@ -255,18 +255,18 @@ const exported = await brain.export({ format: 'json' })
|
|||
### Knowledge Management with Relationships
|
||||
```javascript
|
||||
// Store documentation with rich relationships
|
||||
const apiGuide = await brain.addNoun("REST API Guide", {
|
||||
const apiGuide = await brain.addNoun("REST API Guide", 'document', {
|
||||
title: "API Guide",
|
||||
category: "documentation",
|
||||
version: "2.0"
|
||||
})
|
||||
|
||||
const author = await brain.addNoun("Jane Developer", {
|
||||
const author = await brain.addNoun("Jane Developer", 'person', {
|
||||
type: "person",
|
||||
role: "tech-lead"
|
||||
})
|
||||
|
||||
const project = await brain.addNoun("E-commerce Platform", {
|
||||
const project = await brain.addNoun("E-commerce Platform", 'project', {
|
||||
type: "project",
|
||||
status: "active"
|
||||
})
|
||||
|
|
@ -295,18 +295,18 @@ const similar = await brain.search(existingContent, {
|
|||
### AI Memory Layer with Context
|
||||
```javascript
|
||||
// Store conversation with relationships
|
||||
const userId = await brain.addNoun("User 123", {
|
||||
const userId = await brain.addNoun("User 123", 'user', {
|
||||
type: "user",
|
||||
tier: "premium"
|
||||
})
|
||||
|
||||
const messageId = await brain.addNoun(userMessage, {
|
||||
const messageId = await brain.addNoun(userMessage, 'message', {
|
||||
type: "message",
|
||||
timestamp: Date.now(),
|
||||
session: "abc"
|
||||
})
|
||||
|
||||
const topicId = await brain.addNoun("Product Support", {
|
||||
const topicId = await brain.addNoun("Product Support", 'topic', {
|
||||
type: "topic",
|
||||
category: "support"
|
||||
})
|
||||
|
|
@ -431,7 +431,7 @@ for (const cluster of feedbackClusters) {
|
|||
}
|
||||
|
||||
// Find related documents
|
||||
const docId = await brain.addNoun("Machine learning guide")
|
||||
const docId = await brain.addNoun("Machine learning guide", 'document')
|
||||
const similar = await neural.neighbors(docId, 5)
|
||||
// Returns 5 most similar documents
|
||||
|
||||
|
|
|
|||
77
demo-neural-type-inference.js
Normal file
77
demo-neural-type-inference.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Demo: Neural Type Inference vs Basic Pattern Matching
|
||||
import {
|
||||
inferNounTypeFromMetadata,
|
||||
inferNounTypeNeural
|
||||
} from './dist/utils/typeValidation.js'
|
||||
|
||||
console.log('🧠 Brainy Type Inference: Pattern vs Neural\n')
|
||||
console.log('=' .repeat(50) + '\n')
|
||||
|
||||
// Test cases showing the difference
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Simple Person (both work)',
|
||||
data: {
|
||||
email: 'john@example.com',
|
||||
name: 'John Doe'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Complex Role (neural understands context)',
|
||||
data: {
|
||||
title: 'Engineering Manager',
|
||||
responsibilities: 'Leads team, reviews code, mentors developers',
|
||||
department: 'Technology'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Ambiguous Entity (neural uses semantic understanding)',
|
||||
data: {
|
||||
name: 'Tesla',
|
||||
founded: 2003,
|
||||
employees: 127855,
|
||||
products: ['Model S', 'Model 3', 'Model X']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Scientific Content (neural recognizes research)',
|
||||
data: {
|
||||
title: 'Effects of quantum entanglement on superconductivity',
|
||||
abstract: 'This study examines the relationship between quantum states...',
|
||||
methodology: 'Double-blind controlled experiment',
|
||||
results: 'Statistical significance p<0.05'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Legal Document (neural understands context)',
|
||||
data: {
|
||||
parties: ['Company A', 'Company B'],
|
||||
effectiveDate: '2024-01-01',
|
||||
terms: 'Non-disclosure agreement',
|
||||
jurisdiction: 'California'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
async function runComparison() {
|
||||
for (const testCase of testCases) {
|
||||
console.log(`📝 Test: ${testCase.name}`)
|
||||
console.log(` Data: ${JSON.stringify(testCase.data, null, 2).split('\n').join('\n ')}`)
|
||||
|
||||
// Basic pattern matching (synchronous)
|
||||
const basicType = inferNounTypeFromMetadata(testCase.data)
|
||||
console.log(` 🔍 Basic Pattern Match: ${basicType || 'content (default)'}`)
|
||||
|
||||
// Neural inference (async, uses embeddings)
|
||||
const neuralType = await inferNounTypeNeural(testCase.data)
|
||||
console.log(` 🧠 Neural Inference: ${neuralType}`)
|
||||
|
||||
if (basicType !== neuralType) {
|
||||
console.log(` ✨ Neural found better match!`)
|
||||
}
|
||||
|
||||
console.log()
|
||||
}
|
||||
}
|
||||
|
||||
runComparison().catch(console.error)
|
||||
71
demo-strict-types.js
Normal file
71
demo-strict-types.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// Demo: Strict Type Enforcement (Now Default!)
|
||||
import { BrainyData, NounType } from './dist/index.js'
|
||||
|
||||
async function demo() {
|
||||
console.log('🚨 Brainy 3.0: Strict Types by Default!\n')
|
||||
console.log('=' .repeat(50) + '\n')
|
||||
|
||||
// Create instance with default config (STRICT MODE)
|
||||
const brain = new BrainyData({
|
||||
storage: { forceMemoryStorage: true }
|
||||
})
|
||||
await brain.init()
|
||||
|
||||
console.log('❌ Test 1: Old API fails by default')
|
||||
console.log('----------------------------------------')
|
||||
try {
|
||||
// This WILL FAIL - no type specified
|
||||
await brain.addNoun('Some data', { metadata: 'stuff' })
|
||||
} catch (error) {
|
||||
console.log('Error (expected):', error.message.split('\n')[0])
|
||||
console.log('✅ Good! Forces you to specify type.\n')
|
||||
}
|
||||
|
||||
console.log('✅ Test 2: New API with explicit types works')
|
||||
console.log('---------------------------------------------')
|
||||
|
||||
const personId = await brain.addNoun(
|
||||
'John Doe',
|
||||
NounType.Person,
|
||||
{ role: 'Engineer' }
|
||||
)
|
||||
console.log(`Added person with ID: ${personId}`)
|
||||
|
||||
const docId = await brain.addNoun(
|
||||
'API Documentation',
|
||||
NounType.Document,
|
||||
{ version: '2.0' }
|
||||
)
|
||||
console.log(`Added document with ID: ${docId}\n`)
|
||||
|
||||
console.log('🔄 Test 3: Compatibility mode (opt-in only)')
|
||||
console.log('--------------------------------------------')
|
||||
|
||||
// Must explicitly enable compatibility mode
|
||||
const compatBrain = new BrainyData({
|
||||
storage: { forceMemoryStorage: true },
|
||||
typeCompatibilityMode: true, // EXPLICIT OPT-IN
|
||||
logging: { verbose: false }
|
||||
})
|
||||
await compatBrain.init()
|
||||
|
||||
// Now old API works (with warnings if verbose: true)
|
||||
const oldApiId = await compatBrain.addNoun(
|
||||
'Old style data',
|
||||
{ someField: 'value' }
|
||||
)
|
||||
console.log(`Compatibility mode allows old API: ${oldApiId}\n`)
|
||||
|
||||
console.log('📊 Summary: Why Strict Mode is Better')
|
||||
console.log('--------------------------------------')
|
||||
console.log('1. Forces explicit types → Better data quality')
|
||||
console.log('2. No ambiguous "content" everywhere')
|
||||
console.log('3. AI works better with typed data')
|
||||
console.log('4. Prevents technical debt')
|
||||
console.log('5. Can always opt-in to compatibility if needed')
|
||||
console.log('\n✨ Brainy 3.0: Type Safety First!')
|
||||
}
|
||||
|
||||
// Bypass version check for demo
|
||||
process.env.BRAINY_SKIP_VERSION_CHECK = 'true'
|
||||
demo().catch(console.error)
|
||||
107
demo-type-enforcement.js
Normal file
107
demo-type-enforcement.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Demo: Type Enforcement in Brainy
|
||||
import { BrainyData, NounType, VerbType } from './dist/index.js'
|
||||
|
||||
async function demo() {
|
||||
console.log('🧠 Brainy Type Enforcement Demo\n')
|
||||
console.log('================================\n')
|
||||
|
||||
// Create instance in compatibility mode (default)
|
||||
const brain = new BrainyData({
|
||||
storage: { forceMemoryStorage: true },
|
||||
logging: { verbose: true } // Show warnings
|
||||
})
|
||||
await brain.init()
|
||||
|
||||
console.log('📝 Test 1: New API with explicit types')
|
||||
console.log('----------------------------------------')
|
||||
|
||||
// New API - explicit types
|
||||
const personId = await brain.addNoun(
|
||||
'John Doe is a software engineer',
|
||||
NounType.Person,
|
||||
{ role: 'Engineer', experience: 5 }
|
||||
)
|
||||
console.log(`✅ Added person with ID: ${personId}\n`)
|
||||
|
||||
const docId = await brain.addNoun(
|
||||
'Technical documentation for the API',
|
||||
NounType.Document,
|
||||
{ title: 'API Docs', version: '2.0' }
|
||||
)
|
||||
console.log(`✅ Added document with ID: ${docId}\n`)
|
||||
|
||||
console.log('📝 Test 2: Old API with deprecation warning')
|
||||
console.log('--------------------------------------------')
|
||||
|
||||
// Old API - will show deprecation warning
|
||||
const contentId = await brain.addNoun(
|
||||
'Some content without explicit type',
|
||||
{ description: 'This uses the old API' }
|
||||
)
|
||||
console.log(`✅ Added content with ID: ${contentId}\n`)
|
||||
|
||||
console.log('📝 Test 3: Type inference from metadata')
|
||||
console.log('----------------------------------------')
|
||||
|
||||
// Will infer Person type from email
|
||||
const userId = await brain.addNoun(
|
||||
'Jane Smith profile',
|
||||
{ email: 'jane@example.com', username: 'jsmith' }
|
||||
)
|
||||
console.log(`✅ Added user (inferred Person type) with ID: ${userId}\n`)
|
||||
|
||||
console.log('📝 Test 4: Invalid type with helpful suggestion')
|
||||
console.log('------------------------------------------------')
|
||||
|
||||
try {
|
||||
// Typo in type name
|
||||
await brain.addNoun(
|
||||
'Test data',
|
||||
'persan', // Typo!
|
||||
{}
|
||||
)
|
||||
} catch (error) {
|
||||
console.log(`❌ Error (as expected): ${error.message}\n`)
|
||||
}
|
||||
|
||||
console.log('📝 Test 5: Strict mode enforcement')
|
||||
console.log('-----------------------------------')
|
||||
|
||||
// Create new instance in strict mode
|
||||
const strictBrain = new BrainyData({
|
||||
storage: { forceMemoryStorage: true },
|
||||
typeCompatibilityMode: false, // Strict mode!
|
||||
logging: { verbose: false }
|
||||
})
|
||||
await strictBrain.init()
|
||||
|
||||
try {
|
||||
// This will fail in strict mode
|
||||
await strictBrain.addNoun('Test', { meta: 'data' })
|
||||
} catch (error) {
|
||||
console.log(`❌ Strict mode error (as expected): ${error.message}\n`)
|
||||
}
|
||||
|
||||
// This will work in strict mode
|
||||
const strictId = await strictBrain.addNoun(
|
||||
'Valid data with type',
|
||||
NounType.Content,
|
||||
{ valid: true }
|
||||
)
|
||||
console.log(`✅ Strict mode success with ID: ${strictId}\n`)
|
||||
|
||||
console.log('📝 Test 6: Verify types are stored correctly')
|
||||
console.log('---------------------------------------------')
|
||||
|
||||
const person = await brain.getNoun(personId)
|
||||
console.log(`Person noun type: ${person.metadata.noun}`)
|
||||
console.log(`Person metadata:`, person.metadata)
|
||||
|
||||
const doc = await brain.getNoun(docId)
|
||||
console.log(`\nDocument noun type: ${doc.metadata.noun}`)
|
||||
console.log(`Document metadata:`, doc.metadata)
|
||||
|
||||
console.log('\n✨ Demo complete!')
|
||||
}
|
||||
|
||||
demo().catch(console.error)
|
||||
|
|
@ -34,10 +34,10 @@ That's it! No configuration needed. Brainy automatically:
|
|||
|
||||
```javascript
|
||||
// Add a simple string
|
||||
await brain.addNoun("JavaScript is a versatile programming language")
|
||||
await brain.addNoun("JavaScript is a versatile programming language", 'concept')
|
||||
|
||||
// Add with metadata
|
||||
await brain.addNoun("React is a JavaScript library", {
|
||||
await brain.addNoun("React is a JavaScript library", 'concept', {
|
||||
type: "library",
|
||||
category: "frontend",
|
||||
popularity: "high"
|
||||
|
|
@ -48,7 +48,7 @@ await brain.addNoun({
|
|||
title: "Introduction to TypeScript",
|
||||
content: "TypeScript adds static typing to JavaScript",
|
||||
author: "John Doe"
|
||||
}, {
|
||||
}, 'document', {
|
||||
type: "article",
|
||||
date: "2024-01-15"
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const brain = new BrainyData() // Zero config!
|
|||
await brain.init()
|
||||
|
||||
// Add data (text auto-embeds!)
|
||||
await brain.addNoun('The future of AI is here')
|
||||
await brain.addNoun('The future of AI is here', 'content')
|
||||
|
||||
// Search with Triple Intelligence
|
||||
const results = await brain.find({
|
||||
|
|
@ -322,9 +322,9 @@ const articles = await brain.find({
|
|||
### Creating Knowledge Graphs
|
||||
```typescript
|
||||
// Add entities
|
||||
const ai = await brain.addNoun('Artificial Intelligence')
|
||||
const ml = await brain.addNoun('Machine Learning')
|
||||
const dl = await brain.addNoun('Deep Learning')
|
||||
const ai = await brain.addNoun('Artificial Intelligence', 'concept')
|
||||
const ml = await brain.addNoun('Machine Learning', 'concept')
|
||||
const dl = await brain.addNoun('Deep Learning', 'concept')
|
||||
|
||||
// Create relationships
|
||||
await brain.addVerb(ml, ai, 'subset_of')
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ const id = await brain.addNoun("The quick brown fox jumps over the lazy dog", {
|
|||
console.log(`Added noun with ID: ${id}`)
|
||||
|
||||
// Add relationships (verbs) between entities
|
||||
const sourceId = await brain.addNoun("John Smith")
|
||||
const targetId = await brain.addNoun("TechCorp")
|
||||
const sourceId = await brain.addNoun("John Smith", 'person')
|
||||
const targetId = await brain.addNoun("TechCorp", 'organization')
|
||||
await brain.addVerb(sourceId, targetId, "works_at", {
|
||||
position: "Engineer",
|
||||
since: "2024"
|
||||
|
|
@ -140,8 +140,8 @@ const interactionId = await brain.addNoun("user viewed product", {
|
|||
})
|
||||
|
||||
// Create relationships between users and products
|
||||
const userId = await brain.addNoun("user123")
|
||||
const productId = await brain.addNoun("product456")
|
||||
const userId = await brain.addNoun("user123", 'user')
|
||||
const productId = await brain.addNoun("product456", 'product')
|
||||
await brain.addVerb(userId, productId, "viewed", {
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ const response = await openai.embeddings.create({
|
|||
// After: Local Brainy embeddings
|
||||
const brain = new BrainyData()
|
||||
await brain.init() // One-time setup
|
||||
const id = await brain.add("Your text") // Embedded automatically
|
||||
const id = await brain.addNoun("Your text", 'content') // Embedded automatically
|
||||
```
|
||||
|
||||
### From Sentence Transformers
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ const brain = new BrainyData({
|
|||
|
||||
2. **Verify embedding generation**
|
||||
```typescript
|
||||
const id = await brain.add("test content")
|
||||
const id = await brain.addNoun("test content", 'content')
|
||||
const item = await brain.get(id)
|
||||
console.log('Item:', item) // Should have metadata and vector
|
||||
```
|
||||
|
|
@ -193,7 +193,7 @@ const brain = new BrainyData({
|
|||
3. **Check data quality**
|
||||
```typescript
|
||||
// Ensure consistent, descriptive content
|
||||
await brain.add("Domestic cat - small carnivorous mammal", {
|
||||
await brain.addNoun("Domestic cat - small carnivorous mammal", 'content', {
|
||||
category: "animals",
|
||||
subcategory: "pets"
|
||||
})
|
||||
|
|
@ -250,7 +250,7 @@ const brain = new BrainyData({
|
|||
// Process in batches instead of loading all at once
|
||||
for (let i = 0; i < data.length; i += 100) {
|
||||
const batch = data.slice(i, i + 100)
|
||||
await Promise.all(batch.map(item => brain.add(item)))
|
||||
await Promise.all(batch.map(item => brain.addNoun(item, 'content')))
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -366,7 +366,7 @@ try {
|
|||
const brain = new BrainyData()
|
||||
await brain.init()
|
||||
|
||||
const id = await brain.add("health check")
|
||||
const id = await brain.addNoun("health check", 'content')
|
||||
const results = await brain.search("health")
|
||||
|
||||
console.log('✅ Brainy is working correctly')
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ async function main() {
|
|||
await brain.init()
|
||||
|
||||
// 2. Add some sample data
|
||||
await brain.add("The quick brown fox", { type: "sentence", category: "animals" })
|
||||
await brain.add("Machine learning models", { type: "tech", category: "AI" })
|
||||
await brain.add("Natural language processing", { type: "tech", category: "NLP" })
|
||||
await brain.addNoun("The quick brown fox", 'Content', { type: "sentence", category: "animals" })
|
||||
await brain.addNoun("Machine learning models", 'Content', { type: "tech", category: "AI" })
|
||||
await brain.addNoun("Natural language processing", 'Content', { type: "tech", category: "NLP" })
|
||||
|
||||
// 3. Create and register the API Server augmentation
|
||||
const apiServer = new APIServerAugmentation({
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ async function main() {
|
|||
await brain.init()
|
||||
|
||||
// Use Brainy normally - augmentations work transparently
|
||||
await brain.add("Hello world", { type: "greeting" })
|
||||
await brain.addNoun("Hello world", 'Content', { type: "greeting" })
|
||||
|
||||
// Batch operations automatically optimized
|
||||
await brain.addBatch([
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@soulcraft/brainy",
|
||||
"version": "2.10.1",
|
||||
"version": "2.11.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@soulcraft/brainy",
|
||||
"version": "2.10.1",
|
||||
"version": "2.11.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.540.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@soulcraft/brainy",
|
||||
"version": "2.10.1",
|
||||
"version": "2.11.0",
|
||||
"description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ export class APIServerAugmentation extends BaseAugmentation {
|
|||
app.post('/api/add', async (req: any, res: any) => {
|
||||
try {
|
||||
const { content, metadata } = req.body
|
||||
const id = await this.context!.brain.add(content, metadata)
|
||||
const id = await this.context!.brain.addNoun(content, 'Content', metadata)
|
||||
res.json({ success: true, id })
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ success: false, error: error.message })
|
||||
|
|
@ -367,7 +367,7 @@ export class APIServerAugmentation extends BaseAugmentation {
|
|||
break
|
||||
|
||||
case 'add':
|
||||
const id = await this.context!.brain.add(msg.content, msg.metadata)
|
||||
const id = await this.context!.brain.addNoun(msg.content, 'Content', msg.metadata)
|
||||
socket.send(JSON.stringify({
|
||||
type: 'addResult',
|
||||
requestId: msg.requestId,
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ export function getFieldPatterns(entityType: 'noun' | 'verb', specificType?: str
|
|||
|
||||
/**
|
||||
* Priority fields for different entity types (for AI analysis)
|
||||
* Used by the IntelligentTypeMatcher and neural processing
|
||||
* Used by the BrainyTypes and neural processing
|
||||
*/
|
||||
export const TYPE_PRIORITY_FIELDS: Record<string, string[]> = {
|
||||
[NounType.Person]: [
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Universal Display Augmentation - Intelligent Computation Engine
|
||||
*
|
||||
* Leverages existing Brainy AI infrastructure for intelligent field computation:
|
||||
* - IntelligentTypeMatcher for semantic type detection
|
||||
* - BrainyTypes for semantic type detection
|
||||
* - Neural Import patterns for field analysis
|
||||
* - JSON processing utilities for field extraction
|
||||
* - Existing NounType/VerbType taxonomy (31+40 types)
|
||||
|
|
@ -15,7 +15,7 @@ import type {
|
|||
DisplayConfig
|
||||
} from './types.js'
|
||||
import type { VectorDocument, GraphVerb } from '../../coreTypes.js'
|
||||
import { IntelligentTypeMatcher, getTypeMatcher } from '../typeMatching/intelligentTypeMatcher.js'
|
||||
import { BrainyTypes, getBrainyTypes } from '../typeMatching/brainyTypes.js'
|
||||
import { getNounIcon, getVerbIcon } from './iconMappings.js'
|
||||
import {
|
||||
getFieldPatterns,
|
||||
|
|
@ -31,7 +31,7 @@ import { NounType, VerbType } from '../../types/graphTypes.js'
|
|||
* Coordinates AI-powered analysis with fallback heuristics
|
||||
*/
|
||||
export class IntelligentComputationEngine {
|
||||
private typeMatcher: IntelligentTypeMatcher | null = null
|
||||
private typeMatcher: BrainyTypes | null = null
|
||||
private config: DisplayConfig
|
||||
private initialized = false
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ export class IntelligentComputationEngine {
|
|||
|
||||
try {
|
||||
// 🧠 LEVERAGE YOUR EXISTING AI INFRASTRUCTURE
|
||||
this.typeMatcher = await getTypeMatcher()
|
||||
this.typeMatcher = await getBrainyTypes()
|
||||
if (this.typeMatcher) {
|
||||
console.log('🎨 Display computation engine initialized with AI intelligence')
|
||||
} else {
|
||||
|
|
@ -118,7 +118,7 @@ export class IntelligentComputationEngine {
|
|||
}
|
||||
|
||||
/**
|
||||
* AI-powered computation using your existing IntelligentTypeMatcher
|
||||
* AI-powered computation using your existing BrainyTypes
|
||||
* @param data Entity data/metadata
|
||||
* @param entityType Type of entity (noun/verb)
|
||||
* @param options Additional options
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export interface FieldComputationContext {
|
|||
}
|
||||
|
||||
/**
|
||||
* Type matching result from IntelligentTypeMatcher
|
||||
* Type matching result from BrainyTypes
|
||||
*/
|
||||
export interface TypeMatchResult {
|
||||
type: string
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { BaseAugmentation, AugmentationContext } from './brainyAugmentation.js'
|
|||
import { NounType, VerbType } from '../types/graphTypes.js'
|
||||
import * as fs from '../universal/fs.js'
|
||||
import * as path from '../universal/path.js'
|
||||
import { IntelligentTypeMatcher, getTypeMatcher } from './typeMatching/intelligentTypeMatcher.js'
|
||||
import { BrainyTypes, getBrainyTypes } from './typeMatching/brainyTypes.js'
|
||||
import { prodLog } from '../utils/logger.js'
|
||||
|
||||
// Neural Import Analysis Types
|
||||
|
|
@ -74,7 +74,7 @@ export class NeuralImportAugmentation extends BaseAugmentation {
|
|||
|
||||
private config: NeuralImportConfig
|
||||
private analysisCache = new Map<string, NeuralAnalysisResult>()
|
||||
private typeMatcher: IntelligentTypeMatcher | null = null
|
||||
private typeMatcher: BrainyTypes | null = null
|
||||
|
||||
constructor(config: Partial<NeuralImportConfig> = {}) {
|
||||
super()
|
||||
|
|
@ -89,7 +89,7 @@ export class NeuralImportAugmentation extends BaseAugmentation {
|
|||
|
||||
protected async onInitialize(): Promise<void> {
|
||||
try {
|
||||
this.typeMatcher = await getTypeMatcher()
|
||||
this.typeMatcher = await getBrainyTypes()
|
||||
this.log('🧠 Neural Import augmentation initialized with intelligent type matching')
|
||||
} catch (error) {
|
||||
this.log('⚠️ Failed to initialize type matcher, falling back to heuristics', 'warn')
|
||||
|
|
@ -460,7 +460,7 @@ export class NeuralImportAugmentation extends BaseAugmentation {
|
|||
private async inferNounType(obj: any): Promise<string> {
|
||||
if (!this.typeMatcher) {
|
||||
// Initialize type matcher if not available
|
||||
this.typeMatcher = await getTypeMatcher()
|
||||
this.typeMatcher = await getBrainyTypes()
|
||||
}
|
||||
|
||||
const result = await this.typeMatcher.matchNounType(obj)
|
||||
|
|
@ -516,7 +516,7 @@ export class NeuralImportAugmentation extends BaseAugmentation {
|
|||
private async inferVerbType(fieldName: string, sourceObj?: any, targetObj?: any): Promise<string> {
|
||||
if (!this.typeMatcher) {
|
||||
// Initialize type matcher if not available
|
||||
this.typeMatcher = await getTypeMatcher()
|
||||
this.typeMatcher = await getBrainyTypes()
|
||||
}
|
||||
|
||||
const result = await this.typeMatcher.matchVerbType(sourceObj, targetObj, fieldName)
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ export abstract class SynapseAugmentation extends BaseAugmentation {
|
|||
|
||||
// Store original content with neural metadata
|
||||
if (typeof content === 'string') {
|
||||
await this.context.brain.add(content, {
|
||||
await this.context.brain.addNoun(content, 'Content', {
|
||||
...enrichedMetadata,
|
||||
_neuralProcessed: true,
|
||||
_neuralConfidence: neuralResult.data.confidence,
|
||||
|
|
@ -283,10 +283,10 @@ export abstract class SynapseAugmentation extends BaseAugmentation {
|
|||
|
||||
// Fallback to basic storage
|
||||
if (typeof content === 'string') {
|
||||
await this.context.brain.add(content, enrichedMetadata)
|
||||
await this.context.brain.addNoun(content, 'Content', enrichedMetadata)
|
||||
} else {
|
||||
// For structured data, store as JSON
|
||||
await this.context.brain.add(JSON.stringify(content), enrichedMetadata)
|
||||
await this.context.brain.addNoun(JSON.stringify(content), 'Content', enrichedMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Intelligent Type Matcher - Uses embeddings for semantic type detection
|
||||
* BrainyTypes - Intelligent type detection using semantic embeddings
|
||||
*
|
||||
* This module uses our existing TransformerEmbedding and similarity functions
|
||||
* to intelligently match data to our 31 noun types and 40 verb types.
|
||||
|
|
@ -139,9 +139,9 @@ export interface TypeMatchResult {
|
|||
}
|
||||
|
||||
/**
|
||||
* Intelligent Type Matcher using semantic embeddings
|
||||
* BrainyTypes - Intelligent type detection for nouns and verbs
|
||||
*/
|
||||
export class IntelligentTypeMatcher {
|
||||
export class BrainyTypes {
|
||||
private embedder: TransformerEmbedding
|
||||
private nounEmbeddings: Map<string, Vector> = new Map()
|
||||
private verbEmbeddings: Map<string, Vector> = new Map()
|
||||
|
|
@ -520,15 +520,15 @@ export class IntelligentTypeMatcher {
|
|||
/**
|
||||
* Singleton instance for efficient reuse
|
||||
*/
|
||||
let globalMatcher: IntelligentTypeMatcher | null = null
|
||||
let globalInstance: BrainyTypes | null = null
|
||||
|
||||
/**
|
||||
* Get or create the global type matcher instance
|
||||
* Get or create the global BrainyTypes instance
|
||||
*/
|
||||
export async function getTypeMatcher(): Promise<IntelligentTypeMatcher> {
|
||||
if (!globalMatcher) {
|
||||
globalMatcher = new IntelligentTypeMatcher()
|
||||
await globalMatcher.init()
|
||||
export async function getBrainyTypes(): Promise<BrainyTypes> {
|
||||
if (!globalInstance) {
|
||||
globalInstance = new BrainyTypes()
|
||||
await globalInstance.init()
|
||||
}
|
||||
return globalMatcher
|
||||
return globalInstance
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
* 🎨 Provides intelligent display fields for any noun or verb using AI-powered analysis
|
||||
*
|
||||
* Features:
|
||||
* - ✅ Leverages existing IntelligentTypeMatcher for semantic type detection
|
||||
* - ✅ Leverages existing BrainyTypes for semantic type detection
|
||||
* - ✅ Complete icon coverage for all 31 NounTypes + 40+ VerbTypes
|
||||
* - ✅ Zero performance impact with lazy computation and intelligent caching
|
||||
* - ✅ Perfect isolation - can be disabled, replaced, or configured
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ import {
|
|||
} from './utils/metadataNamespace.js'
|
||||
import { PeriodicCleanup, CleanupConfig, CleanupStats } from './utils/periodicCleanup.js'
|
||||
import { NounType, VerbType, GraphNoun } from './types/graphTypes.js'
|
||||
import {
|
||||
validateNounType,
|
||||
validateVerbType
|
||||
} from './utils/typeValidation.js'
|
||||
import {
|
||||
ServerSearchConduitAugmentation,
|
||||
createServerSearchAugmentations
|
||||
|
|
@ -185,6 +189,7 @@ export interface BrainyDataConfig {
|
|||
*/
|
||||
lazyLoadInReadOnlyMode?: boolean
|
||||
|
||||
|
||||
/**
|
||||
* Set the database to write-only mode
|
||||
* When true, the index is not loaded into memory and search operations will throw an error
|
||||
|
|
@ -671,7 +676,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
*/
|
||||
constructor(config: BrainyDataConfig | string | any = {}) {
|
||||
// Enforce Node.js version requirement for ONNX stability
|
||||
if (typeof process !== 'undefined' && process.version) {
|
||||
if (typeof process !== 'undefined' && process.version && !process.env.BRAINY_SKIP_VERSION_CHECK) {
|
||||
enforceNodeVersion()
|
||||
}
|
||||
|
||||
|
|
@ -2054,383 +2059,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to the database with intelligent processing
|
||||
*
|
||||
* @param vectorOrData Vector or data to add
|
||||
* @param metadata Optional metadata to associate with the data
|
||||
* @param options Additional options for processing
|
||||
* @returns The ID of the added data
|
||||
*
|
||||
* @example
|
||||
* // Auto mode - intelligently decides processing
|
||||
* await brainy.add("Customer feedback: Great product!")
|
||||
*
|
||||
* @example
|
||||
* // Explicit literal mode for sensitive data
|
||||
* await brainy.add("API_KEY=secret123", null, { process: 'literal' })
|
||||
*
|
||||
* @example
|
||||
* // Force neural processing
|
||||
* await brainy.add("John works at Acme Corp", null, { process: 'neural' })
|
||||
*/
|
||||
public async add(
|
||||
vectorOrData: Vector | any,
|
||||
metadata?: T,
|
||||
options: {
|
||||
forceEmbed?: boolean // Force using the embedding function even if input is a vector
|
||||
addToRemote?: boolean // Whether to also add to the remote server if connected
|
||||
id?: string // Optional ID to use instead of generating a new one
|
||||
service?: string // The service that is inserting the data
|
||||
process?: 'auto' | 'literal' | 'neural' // Processing mode (default: 'auto')
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Check if database is in read-only mode
|
||||
this.checkReadOnly()
|
||||
|
||||
// Validate input is not null or undefined
|
||||
if (vectorOrData === null || vectorOrData === undefined) {
|
||||
throw new Error('Input cannot be null or undefined')
|
||||
}
|
||||
|
||||
try {
|
||||
let vector: Vector
|
||||
|
||||
// First validate if input is an array but contains non-numeric values
|
||||
if (Array.isArray(vectorOrData)) {
|
||||
for (let i = 0; i < vectorOrData.length; i++) {
|
||||
if (typeof vectorOrData[i] !== 'number') {
|
||||
throw new Error('Vector contains non-numeric values')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if input is already a vector
|
||||
if (Array.isArray(vectorOrData) && !options.forceEmbed) {
|
||||
// Input is already a vector (and we've validated it contains only numbers)
|
||||
vector = vectorOrData
|
||||
} else {
|
||||
// Input needs to be vectorized
|
||||
try {
|
||||
// Check if input is a JSON object and process it specially
|
||||
if (
|
||||
typeof vectorOrData === 'object' &&
|
||||
vectorOrData !== null &&
|
||||
!Array.isArray(vectorOrData)
|
||||
) {
|
||||
// Process JSON object for better vectorization
|
||||
const preparedText = prepareJsonForVectorization(vectorOrData, {
|
||||
// Prioritize common name/title fields if they exist
|
||||
priorityFields: [
|
||||
'name',
|
||||
'title',
|
||||
'company',
|
||||
'organization',
|
||||
'description',
|
||||
'summary'
|
||||
]
|
||||
})
|
||||
vector = await this.embeddingFunction(preparedText)
|
||||
|
||||
// IMPORTANT: When an object is passed as data and no metadata is provided,
|
||||
// use the object AS the metadata too. This is expected behavior for the API.
|
||||
// Users can pass either:
|
||||
// 1. addNoun(string, metadata) - vectorize string, store metadata
|
||||
// 2. addNoun(object) - vectorize object text, store object as metadata
|
||||
// 3. addNoun(object, metadata) - vectorize object text, store provided metadata
|
||||
if (!metadata) {
|
||||
metadata = vectorOrData as T
|
||||
}
|
||||
|
||||
// Track field names for this JSON document
|
||||
const service = this.getServiceName(options)
|
||||
if (this.storage) {
|
||||
await this.storage.trackFieldNames(vectorOrData, service)
|
||||
}
|
||||
} else {
|
||||
// Use standard embedding for non-JSON data
|
||||
vector = await this.embeddingFunction(vectorOrData)
|
||||
}
|
||||
} catch (embedError) {
|
||||
throw new Error(`Failed to vectorize data: ${embedError}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if vector is defined
|
||||
if (!vector) {
|
||||
throw new Error('Vector is undefined or null')
|
||||
}
|
||||
|
||||
// Validate vector dimensions
|
||||
if (vector.length !== this._dimensions) {
|
||||
throw new Error(
|
||||
`Vector dimension mismatch: expected ${this._dimensions}, got ${vector.length}`
|
||||
)
|
||||
}
|
||||
|
||||
// Use ID from options if it exists, otherwise from metadata, otherwise generate a new UUID
|
||||
const id =
|
||||
options.id ||
|
||||
(metadata && typeof metadata === 'object' && 'id' in metadata
|
||||
? (metadata as any).id
|
||||
: uuidv4())
|
||||
|
||||
// Check for existing noun (both write-only and normal modes)
|
||||
let existingNoun: HNSWNoun | undefined
|
||||
if (options.id) {
|
||||
try {
|
||||
if (this.writeOnly) {
|
||||
// In write-only mode, check storage directly
|
||||
existingNoun =
|
||||
(await this.storage!.getNoun(options.id)) ?? undefined
|
||||
} else {
|
||||
// In normal mode, check index first, then storage
|
||||
existingNoun = this.index.getNouns().get(options.id)
|
||||
if (!existingNoun) {
|
||||
existingNoun =
|
||||
(await this.storage!.getNoun(options.id)) ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (existingNoun) {
|
||||
// Check if existing noun is a placeholder
|
||||
const existingMetadata = await this.storage!.getMetadata(options.id)
|
||||
const isPlaceholder =
|
||||
existingMetadata &&
|
||||
typeof existingMetadata === 'object' &&
|
||||
(existingMetadata as any).isPlaceholder
|
||||
|
||||
if (isPlaceholder) {
|
||||
// Replace placeholder with real data
|
||||
if (this.loggingConfig?.verbose) {
|
||||
console.log(
|
||||
`Replacing placeholder noun ${options.id} with real data`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Real noun already exists, update it
|
||||
if (this.loggingConfig?.verbose) {
|
||||
console.log(`Updating existing noun ${options.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (storageError) {
|
||||
// Item doesn't exist, continue with add operation
|
||||
}
|
||||
}
|
||||
|
||||
let noun: HNSWNoun
|
||||
|
||||
// In write-only mode, skip index operations since index is not loaded
|
||||
if (this.writeOnly) {
|
||||
// Create noun object directly without adding to index
|
||||
noun = {
|
||||
id,
|
||||
vector,
|
||||
connections: new Map(),
|
||||
level: 0, // Default level for new nodes
|
||||
metadata: undefined // Will be set separately
|
||||
}
|
||||
} else {
|
||||
// Normal mode: Add to HNSW index first
|
||||
await this.hnswIndex.addItem({ id, vector, metadata })
|
||||
|
||||
// Get the noun from the HNSW index
|
||||
const indexNoun = this.hnswIndex.getNouns().get(id)
|
||||
if (!indexNoun) {
|
||||
throw new Error(`Failed to retrieve newly created noun with ID ${id}`)
|
||||
}
|
||||
noun = indexNoun
|
||||
}
|
||||
|
||||
// Save noun to storage using augmentation system
|
||||
await this.augmentations.execute('saveNoun', { noun, options }, async () => {
|
||||
await this.storage!.saveNoun(noun)
|
||||
const service = this.getServiceName(options)
|
||||
await this.storage!.incrementStatistic('noun', service)
|
||||
})
|
||||
|
||||
// Save metadata if provided and not empty
|
||||
if (metadata !== undefined) {
|
||||
// Skip saving if metadata is an empty object
|
||||
if (
|
||||
metadata &&
|
||||
typeof metadata === 'object' &&
|
||||
Object.keys(metadata).length === 0
|
||||
) {
|
||||
// Don't save empty metadata
|
||||
// Explicitly save null to ensure no metadata is stored
|
||||
await this.storage!.saveMetadata(id, null)
|
||||
} else {
|
||||
// Validate noun type if metadata is for a GraphNoun
|
||||
if (metadata && typeof metadata === 'object' && 'noun' in metadata) {
|
||||
const nounType = (metadata as unknown as GraphNoun).noun
|
||||
|
||||
// Check if the noun type is valid
|
||||
const isValidNounType = Object.values(NounType).includes(nounType)
|
||||
|
||||
if (!isValidNounType) {
|
||||
console.warn(
|
||||
`Invalid noun type: ${nounType}. Falling back to GraphNoun.`
|
||||
)
|
||||
// Set a default noun type
|
||||
;(metadata as unknown as GraphNoun).noun = NounType.Concept
|
||||
}
|
||||
|
||||
// Ensure createdBy field is populated for GraphNoun
|
||||
const service = options.service || this.getCurrentAugmentation()
|
||||
const graphNoun = metadata as unknown as GraphNoun
|
||||
|
||||
// Only set createdBy if it doesn't exist or is being explicitly updated
|
||||
if (!graphNoun.createdBy || options.service) {
|
||||
graphNoun.createdBy = getAugmentationVersion(service)
|
||||
}
|
||||
|
||||
// Update timestamps
|
||||
const now = new Date()
|
||||
const timestamp = {
|
||||
seconds: Math.floor(now.getTime() / 1000),
|
||||
nanoseconds: (now.getTime() % 1000) * 1000000
|
||||
}
|
||||
|
||||
// Set createdAt if it doesn't exist
|
||||
if (!graphNoun.createdAt) {
|
||||
graphNoun.createdAt = timestamp
|
||||
}
|
||||
|
||||
// Always update updatedAt
|
||||
graphNoun.updatedAt = timestamp
|
||||
}
|
||||
|
||||
// Create properly namespaced metadata for new items
|
||||
let metadataToSave = createNamespacedMetadata(metadata)
|
||||
|
||||
// Add domain metadata if distributed mode is enabled
|
||||
if (this.domainDetector) {
|
||||
// First check if domain is already in metadata
|
||||
if ((metadataToSave as any).domain) {
|
||||
// Domain already specified, keep it
|
||||
const domainInfo =
|
||||
this.domainDetector.detectDomain(metadataToSave)
|
||||
if (domainInfo.domainMetadata) {
|
||||
;(metadataToSave as any).domainMetadata =
|
||||
domainInfo.domainMetadata
|
||||
}
|
||||
} else {
|
||||
// Try to detect domain from the data
|
||||
const dataToAnalyze = Array.isArray(vectorOrData)
|
||||
? metadata
|
||||
: vectorOrData
|
||||
const domainInfo =
|
||||
this.domainDetector.detectDomain(dataToAnalyze)
|
||||
if (domainInfo.domain) {
|
||||
;(metadataToSave as any).domain = domainInfo.domain
|
||||
if (domainInfo.domainMetadata) {
|
||||
;(metadataToSave as any).domainMetadata =
|
||||
domainInfo.domainMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add partition information if distributed mode is enabled
|
||||
if (this.partitioner) {
|
||||
const partition = this.partitioner.getPartition(id)
|
||||
;(metadataToSave as any).partition = partition
|
||||
}
|
||||
|
||||
await this.storage!.saveMetadata(id, metadataToSave)
|
||||
|
||||
// Update metadata index (write-only mode should build indices!)
|
||||
if (this.index && !this.frozen) {
|
||||
await this.metadataIndex?.addToIndex?.(id, metadataToSave)
|
||||
}
|
||||
|
||||
// Track metadata statistics
|
||||
const metadataService = this.getServiceName(options)
|
||||
await this.storage!.incrementStatistic('metadata', metadataService)
|
||||
|
||||
// Track content type if it's a GraphNoun
|
||||
if (
|
||||
metadataToSave &&
|
||||
typeof metadataToSave === 'object' &&
|
||||
'noun' in metadataToSave
|
||||
) {
|
||||
this.metrics.trackContentType(
|
||||
(metadataToSave as any).noun
|
||||
)
|
||||
}
|
||||
|
||||
// Track update timestamp (handled by metrics augmentation)
|
||||
}
|
||||
}
|
||||
|
||||
// Update HNSW index size with actual index size
|
||||
const indexSize = this.index.size()
|
||||
await this.storage!.updateHnswIndexSize(indexSize)
|
||||
|
||||
// Update health metrics if in distributed mode
|
||||
if (this.monitoring) {
|
||||
const vectorCount = await this.getNounCount()
|
||||
this.monitoring.updateVectorCount(vectorCount)
|
||||
}
|
||||
|
||||
// If addToRemote is true and we're connected to a remote server, add to remote as well
|
||||
if (options.addToRemote && this.isConnectedToRemoteServer()) {
|
||||
try {
|
||||
await this.addToRemote(id, vector, metadata)
|
||||
} catch (remoteError) {
|
||||
console.warn(
|
||||
`Failed to add to remote server: ${remoteError}. Continuing with local add.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate search cache since data has changed
|
||||
this.cache?.invalidateOnDataChange('add')
|
||||
|
||||
// Determine processing mode
|
||||
const processingMode = options.process || 'auto'
|
||||
let shouldProcessNeurally = false
|
||||
|
||||
if (processingMode === 'neural') {
|
||||
shouldProcessNeurally = true
|
||||
} else if (processingMode === 'auto') {
|
||||
// Auto-detect whether to use neural processing
|
||||
shouldProcessNeurally = this.shouldAutoProcessNeurally(vectorOrData, metadata)
|
||||
}
|
||||
// 'literal' mode means no neural processing
|
||||
|
||||
// 🧠 AI Processing (Neural Import) - Based on processing mode
|
||||
if (shouldProcessNeurally) {
|
||||
try {
|
||||
// Execute augmentation pipeline for data processing
|
||||
// Note: Augmentations will be called via this.augmentations.execute during the actual add operation
|
||||
// This replaces the legacy SENSE pipeline
|
||||
|
||||
if (this.loggingConfig?.verbose) {
|
||||
console.log(`🧠 AI processing completed for data: ${id}`)
|
||||
}
|
||||
} catch (processingError) {
|
||||
// Don't fail the add operation if processing fails
|
||||
console.warn(`🧠 AI processing failed for ${id}:`, processingError)
|
||||
}
|
||||
}
|
||||
|
||||
return id
|
||||
} catch (error) {
|
||||
console.error('Failed to add vector:', error)
|
||||
|
||||
// Track error in health monitor
|
||||
if (this.monitoring) {
|
||||
this.monitoring.recordRequest(0, true)
|
||||
}
|
||||
|
||||
throw new Error(`Failed to add vector: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVED: addItem() - Use addNoun() instead (cleaner 2.0 API)
|
||||
|
||||
|
|
@ -2488,14 +2117,15 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
* @returns Array of IDs for the added items
|
||||
*/
|
||||
/**
|
||||
* Add multiple nouns in batch
|
||||
* @param items Array of nouns to add
|
||||
* Add multiple nouns in batch with required types
|
||||
* @param items Array of nouns to add (all must have types)
|
||||
* @param options Batch processing options
|
||||
* @returns Array of generated IDs
|
||||
*/
|
||||
public async addNouns(
|
||||
items: Array<{
|
||||
vectorOrData: Vector | any
|
||||
nounType: NounType | string // Always required
|
||||
metadata?: T
|
||||
}>,
|
||||
options: {
|
||||
|
|
@ -2510,6 +2140,30 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
// Check if database is in read-only mode
|
||||
this.checkReadOnly()
|
||||
|
||||
// Validate all types upfront for better error handling
|
||||
const invalidItems: number[] = []
|
||||
|
||||
items.forEach((item, index) => {
|
||||
if (!item.nounType || typeof item.nounType !== 'string') {
|
||||
invalidItems.push(index)
|
||||
} else {
|
||||
// Validate the type is valid
|
||||
try {
|
||||
validateNounType(item.nounType)
|
||||
} catch (error) {
|
||||
invalidItems.push(index)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (invalidItems.length > 0) {
|
||||
throw new Error(
|
||||
`Type validation failed for ${invalidItems.length} items at indices: ${invalidItems.slice(0, 5).join(', ')}${invalidItems.length > 5 ? '...' : ''}\n` +
|
||||
'All items must have valid noun types.\n' +
|
||||
'Example: { vectorOrData: "data", nounType: NounType.Content, metadata: {...} }'
|
||||
)
|
||||
}
|
||||
|
||||
// Default concurrency to 4 if not specified
|
||||
const concurrency = options.concurrency || 4
|
||||
|
||||
|
|
@ -2528,12 +2182,14 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
// Separate items that are already vectors from those that need embedding
|
||||
const vectorItems: Array<{
|
||||
vectorOrData: Vector
|
||||
nounType: NounType | string
|
||||
metadata?: T
|
||||
index: number
|
||||
}> = []
|
||||
|
||||
const textItems: Array<{
|
||||
text: string
|
||||
nounType: NounType | string
|
||||
metadata?: T
|
||||
index: number
|
||||
}> = []
|
||||
|
|
@ -2548,6 +2204,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
// Item is already a vector
|
||||
vectorItems.push({
|
||||
vectorOrData: item.vectorOrData,
|
||||
nounType: item.nounType,
|
||||
metadata: item.metadata,
|
||||
index
|
||||
})
|
||||
|
|
@ -2555,6 +2212,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
// Item is text that needs embedding
|
||||
textItems.push({
|
||||
text: item.vectorOrData,
|
||||
nounType: item.nounType,
|
||||
metadata: item.metadata,
|
||||
index
|
||||
})
|
||||
|
|
@ -2564,6 +2222,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
const textRepresentation = String(item.vectorOrData)
|
||||
textItems.push({
|
||||
text: textRepresentation,
|
||||
nounType: item.nounType,
|
||||
metadata: item.metadata,
|
||||
index
|
||||
})
|
||||
|
|
@ -2571,8 +2230,8 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
})
|
||||
|
||||
// Process vector items (already embedded)
|
||||
const vectorPromises = vectorItems.map((item) =>
|
||||
this.addNoun(item.vectorOrData, item.metadata)
|
||||
const vectorPromises = vectorItems.map((item) =>
|
||||
this.addNoun(item.vectorOrData, item.nounType!, item.metadata)
|
||||
)
|
||||
|
||||
// Process text items in a single batch embedding operation
|
||||
|
|
@ -2585,8 +2244,8 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
const embeddings = await batchEmbed(texts)
|
||||
|
||||
// Add each item with its embedding
|
||||
textPromises = textItems.map((item, i) =>
|
||||
this.addNoun(embeddings[i], item.metadata)
|
||||
textPromises = textItems.map((item, i) =>
|
||||
this.addNoun(embeddings[i], item.nounType!, item.metadata)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -2609,13 +2268,14 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
|
||||
/**
|
||||
* Add multiple vectors or data items to both local and remote databases
|
||||
* @param items Array of items to add
|
||||
* @param items Array of items to add (with required types)
|
||||
* @param options Additional options
|
||||
* @returns Array of IDs for the added items
|
||||
*/
|
||||
public async addBatchToBoth(
|
||||
items: Array<{
|
||||
vectorOrData: Vector | any
|
||||
nounType: NounType | string // Required
|
||||
metadata?: T
|
||||
}>,
|
||||
options: {
|
||||
|
|
@ -6487,8 +6147,13 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
}
|
||||
}
|
||||
|
||||
// Extract type from metadata or default to Content
|
||||
const nounType = (noun.metadata && typeof noun.metadata === 'object' && 'noun' in noun.metadata)
|
||||
? (noun.metadata as any).noun
|
||||
: NounType.Content
|
||||
|
||||
// Add the noun with its vector and metadata (custom ID not supported)
|
||||
await this.addNoun(noun.vector, noun.metadata)
|
||||
await this.addNoun(noun.vector, nounType, noun.metadata)
|
||||
nounsRestored++
|
||||
} catch (error) {
|
||||
console.error(`Failed to restore noun ${noun.id}:`, error)
|
||||
|
|
@ -6681,8 +6346,8 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
}
|
||||
}
|
||||
|
||||
// Add the noun
|
||||
const id = await this.addNoun(metadata.description, metadata as T)
|
||||
// Add the noun with explicit type
|
||||
const id = await this.addNoun(metadata.description, nounType, metadata as T)
|
||||
nounIds.push(id)
|
||||
}
|
||||
|
||||
|
|
@ -6918,8 +6583,7 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
// Use simple text for vectorization
|
||||
const searchableText = `Configuration setting for ${key}`
|
||||
|
||||
await this.addNoun(searchableText, {
|
||||
nounType: NounType.State,
|
||||
await this.addNoun(searchableText, NounType.State, {
|
||||
configKey: key,
|
||||
configValue: configValue,
|
||||
encrypted: !!options?.encrypt,
|
||||
|
|
@ -7078,18 +6742,370 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
* @returns Created noun ID
|
||||
*/
|
||||
/**
|
||||
* Add a noun to the database
|
||||
* Add a noun to the database with required type
|
||||
* Clean 2.0 API - primary method for adding data
|
||||
*
|
||||
* @param vectorOrData Vector array or data to embed
|
||||
* @param metadata Metadata to store with the noun
|
||||
* @param nounType Required noun type (one of 31 types)
|
||||
* @param metadata Optional metadata object
|
||||
* @returns The generated ID
|
||||
*/
|
||||
public async addNoun(
|
||||
vectorOrData: Vector | any,
|
||||
metadata?: T
|
||||
nounType: NounType | string,
|
||||
metadata?: T,
|
||||
options: {
|
||||
forceEmbed?: boolean // Force using the embedding function even if input is a vector
|
||||
addToRemote?: boolean // Whether to also add to the remote server if connected
|
||||
id?: string // Optional ID to use instead of generating a new one
|
||||
service?: string // The service that is inserting the data
|
||||
process?: 'auto' | 'literal' | 'neural' // Processing mode (default: 'auto')
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
return await this.add(vectorOrData, metadata)
|
||||
// Validate noun type
|
||||
const validatedType = validateNounType(nounType)
|
||||
|
||||
// Enrich metadata with validated type
|
||||
let enrichedMetadata = {
|
||||
...metadata,
|
||||
noun: validatedType
|
||||
} as T
|
||||
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Check if database is in read-only mode
|
||||
this.checkReadOnly()
|
||||
|
||||
// Validate input is not null or undefined
|
||||
if (vectorOrData === null || vectorOrData === undefined) {
|
||||
throw new Error('Input cannot be null or undefined')
|
||||
}
|
||||
|
||||
try {
|
||||
let vector: Vector
|
||||
|
||||
if (Array.isArray(vectorOrData)) {
|
||||
for (let i = 0; i < vectorOrData.length; i++) {
|
||||
if (typeof vectorOrData[i] !== 'number') {
|
||||
throw new Error('Vector contains non-numeric values')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if input is already a vector
|
||||
if (Array.isArray(vectorOrData) && !options.forceEmbed) {
|
||||
// Input is already a vector (and we've validated it contains only numbers)
|
||||
vector = vectorOrData
|
||||
} else {
|
||||
// Input needs to be vectorized
|
||||
try {
|
||||
// Check if input is a JSON object and process it specially
|
||||
if (
|
||||
typeof vectorOrData === 'object' &&
|
||||
vectorOrData !== null &&
|
||||
!Array.isArray(vectorOrData)
|
||||
) {
|
||||
// Process JSON object for better vectorization
|
||||
const preparedText = prepareJsonForVectorization(vectorOrData, {
|
||||
// Prioritize common name/title fields if they exist
|
||||
priorityFields: [
|
||||
'name',
|
||||
'title',
|
||||
'company',
|
||||
'organization',
|
||||
'description',
|
||||
'summary'
|
||||
]
|
||||
})
|
||||
vector = await this.embeddingFunction(preparedText)
|
||||
|
||||
// IMPORTANT: When an object is passed as data and no metadata is provided,
|
||||
// use the object AS the metadata too. This is expected behavior for the API.
|
||||
// Users can pass either:
|
||||
// 1. addNoun(string, metadata) - vectorize string, store metadata
|
||||
// 2. addNoun(object) - vectorize object text, store object as metadata
|
||||
// 3. addNoun(object, metadata) - vectorize object text, store provided metadata
|
||||
if (!enrichedMetadata || Object.keys(enrichedMetadata).length === 1) { // Only has 'noun' key
|
||||
enrichedMetadata = { ...vectorOrData, noun: validatedType } as T
|
||||
}
|
||||
|
||||
// Track field names for this JSON document
|
||||
const service = this.getServiceName(options)
|
||||
if (this.storage) {
|
||||
await this.storage.trackFieldNames(vectorOrData, service)
|
||||
}
|
||||
} else {
|
||||
// Use standard embedding for non-JSON data
|
||||
vector = await this.embeddingFunction(vectorOrData)
|
||||
}
|
||||
} catch (embedError) {
|
||||
throw new Error(`Failed to vectorize data: ${embedError}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if vector is defined
|
||||
if (!vector) {
|
||||
throw new Error('Vector is undefined or null')
|
||||
}
|
||||
|
||||
// Validate vector dimensions
|
||||
if (vector.length !== this._dimensions) {
|
||||
throw new Error(
|
||||
`Vector dimension mismatch: expected ${this._dimensions}, got ${vector.length}`
|
||||
)
|
||||
}
|
||||
|
||||
// Use ID from options if it exists, otherwise from metadata, otherwise generate a new UUID
|
||||
const id =
|
||||
options.id ||
|
||||
(enrichedMetadata && typeof enrichedMetadata === 'object' && 'id' in enrichedMetadata
|
||||
? (enrichedMetadata as any).id
|
||||
: uuidv4())
|
||||
|
||||
// Check for existing noun (both write-only and normal modes)
|
||||
let existingNoun: HNSWNoun | undefined
|
||||
if (options.id) {
|
||||
try {
|
||||
if (this.writeOnly) {
|
||||
// In write-only mode, check storage directly
|
||||
existingNoun =
|
||||
(await this.storage!.getNoun(options.id)) ?? undefined
|
||||
} else {
|
||||
// In normal mode, check index first, then storage
|
||||
existingNoun = this.index.getNouns().get(options.id)
|
||||
if (!existingNoun) {
|
||||
existingNoun =
|
||||
(await this.storage!.getNoun(options.id)) ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (existingNoun) {
|
||||
// Check if existing noun is a placeholder
|
||||
const existingMetadata = await this.storage!.getMetadata(options.id)
|
||||
const isPlaceholder =
|
||||
existingMetadata &&
|
||||
typeof existingMetadata === 'object' &&
|
||||
(existingMetadata as any).isPlaceholder
|
||||
|
||||
if (isPlaceholder) {
|
||||
// Replace placeholder with real data
|
||||
if (this.loggingConfig?.verbose) {
|
||||
console.log(
|
||||
`Replacing placeholder noun ${options.id} with real data`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Real noun already exists, update it
|
||||
if (this.loggingConfig?.verbose) {
|
||||
console.log(`Updating existing noun ${options.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (storageError) {
|
||||
// Item doesn't exist, continue with add operation
|
||||
}
|
||||
}
|
||||
|
||||
let noun: HNSWNoun
|
||||
|
||||
// In write-only mode, skip index operations since index is not loaded
|
||||
if (this.writeOnly) {
|
||||
// Create noun object directly without adding to index
|
||||
noun = {
|
||||
id,
|
||||
vector,
|
||||
connections: new Map(),
|
||||
level: 0, // Default level for new nodes
|
||||
metadata: undefined // Will be set separately
|
||||
}
|
||||
} else {
|
||||
// Normal mode: Add to HNSW index first
|
||||
await this.hnswIndex.addItem({ id, vector, metadata: enrichedMetadata })
|
||||
|
||||
// Get the noun from the HNSW index
|
||||
const indexNoun = this.hnswIndex.getNouns().get(id)
|
||||
if (!indexNoun) {
|
||||
throw new Error(`Failed to retrieve newly created noun with ID ${id}`)
|
||||
}
|
||||
noun = indexNoun
|
||||
}
|
||||
|
||||
// Save noun to storage using augmentation system
|
||||
await this.augmentations.execute('saveNoun', { noun, options }, async () => {
|
||||
await this.storage!.saveNoun(noun)
|
||||
const service = this.getServiceName(options)
|
||||
await this.storage!.incrementStatistic('noun', service)
|
||||
})
|
||||
|
||||
// Save metadata if provided and not empty
|
||||
if (enrichedMetadata !== undefined) {
|
||||
// Skip saving if metadata is an empty object
|
||||
if (
|
||||
enrichedMetadata &&
|
||||
typeof enrichedMetadata === 'object' &&
|
||||
Object.keys(enrichedMetadata).length === 0
|
||||
) {
|
||||
// Don't save empty metadata
|
||||
// Explicitly save null to ensure no metadata is stored
|
||||
await this.storage!.saveMetadata(id, null)
|
||||
} else {
|
||||
// Validate noun type if metadata is for a GraphNoun
|
||||
if (enrichedMetadata && typeof enrichedMetadata === 'object' && 'noun' in enrichedMetadata) {
|
||||
const nounType = (enrichedMetadata as unknown as GraphNoun).noun
|
||||
|
||||
// Check if the noun type is valid
|
||||
const isValidNounType = Object.values(NounType).includes(nounType)
|
||||
|
||||
if (!isValidNounType) {
|
||||
console.warn(
|
||||
`Invalid noun type: ${nounType}. Falling back to GraphNoun.`
|
||||
)
|
||||
// Set a default noun type
|
||||
;(enrichedMetadata as unknown as GraphNoun).noun = NounType.Concept
|
||||
}
|
||||
|
||||
// Ensure createdBy field is populated for GraphNoun
|
||||
const service = options.service || this.getCurrentAugmentation()
|
||||
const graphNoun = enrichedMetadata as unknown as GraphNoun
|
||||
|
||||
// Only set createdBy if it doesn't exist or is being explicitly updated
|
||||
if (!graphNoun.createdBy || options.service) {
|
||||
graphNoun.createdBy = getAugmentationVersion(service)
|
||||
}
|
||||
|
||||
// Update timestamps
|
||||
const now = new Date()
|
||||
const timestamp = {
|
||||
seconds: Math.floor(now.getTime() / 1000),
|
||||
nanoseconds: (now.getTime() % 1000) * 1000000
|
||||
}
|
||||
|
||||
// Set createdAt if it doesn't exist
|
||||
if (!graphNoun.createdAt) {
|
||||
graphNoun.createdAt = timestamp
|
||||
}
|
||||
|
||||
// Always update updatedAt
|
||||
graphNoun.updatedAt = timestamp
|
||||
}
|
||||
|
||||
// Create properly namespaced metadata for new items
|
||||
let metadataToSave = createNamespacedMetadata(enrichedMetadata)
|
||||
|
||||
// Add domain metadata if distributed mode is enabled
|
||||
if (this.domainDetector) {
|
||||
// First check if domain is already in metadata
|
||||
if ((metadataToSave as any).domain) {
|
||||
// Domain already specified, keep it
|
||||
const domainInfo =
|
||||
this.domainDetector.detectDomain(metadataToSave)
|
||||
if (domainInfo.domainMetadata) {
|
||||
;(metadataToSave as any).domainMetadata =
|
||||
domainInfo.domainMetadata
|
||||
}
|
||||
} else {
|
||||
// Try to detect domain from the data
|
||||
const dataToAnalyze = Array.isArray(vectorOrData)
|
||||
? enrichedMetadata
|
||||
: vectorOrData
|
||||
const domainInfo =
|
||||
this.domainDetector.detectDomain(dataToAnalyze)
|
||||
if (domainInfo.domain) {
|
||||
;(metadataToSave as any).domain = domainInfo.domain
|
||||
if (domainInfo.domainMetadata) {
|
||||
;(metadataToSave as any).domainMetadata =
|
||||
domainInfo.domainMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add partition information if distributed mode is enabled
|
||||
if (this.partitioner) {
|
||||
const partition = this.partitioner.getPartition(id)
|
||||
;(metadataToSave as any).partition = partition
|
||||
}
|
||||
|
||||
await this.storage!.saveMetadata(id, metadataToSave)
|
||||
|
||||
// Update metadata index (write-only mode should build indices!)
|
||||
if (this.index && !this.frozen) {
|
||||
await this.metadataIndex?.addToIndex?.(id, metadataToSave)
|
||||
}
|
||||
|
||||
// Track metadata statistics
|
||||
const metadataService = this.getServiceName(options)
|
||||
await this.storage!.incrementStatistic('metadata', metadataService)
|
||||
|
||||
// Content type tracking removed - metrics system not initialized
|
||||
|
||||
// Track update timestamp (handled by metrics augmentation)
|
||||
}
|
||||
}
|
||||
|
||||
// Update HNSW index size with actual index size
|
||||
const indexSize = this.index.size()
|
||||
await this.storage!.updateHnswIndexSize(indexSize)
|
||||
|
||||
// Update health metrics if in distributed mode
|
||||
if (this.monitoring) {
|
||||
const vectorCount = await this.getNounCount()
|
||||
this.monitoring.updateVectorCount(vectorCount)
|
||||
}
|
||||
|
||||
// If addToRemote is true and we're connected to a remote server, add to remote as well
|
||||
if (options.addToRemote && this.isConnectedToRemoteServer()) {
|
||||
try {
|
||||
await this.addToRemote(id, vector, enrichedMetadata)
|
||||
} catch (remoteError) {
|
||||
console.warn(
|
||||
`Failed to add to remote server: ${remoteError}. Continuing with local add.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate search cache since data has changed
|
||||
this.cache?.invalidateOnDataChange('add')
|
||||
|
||||
// Determine processing mode
|
||||
const processingMode = options.process || 'auto'
|
||||
let shouldProcessNeurally = false
|
||||
|
||||
if (processingMode === 'neural') {
|
||||
shouldProcessNeurally = true
|
||||
} else if (processingMode === 'auto') {
|
||||
// Auto-detect whether to use neural processing
|
||||
shouldProcessNeurally = this.shouldAutoProcessNeurally(vectorOrData, enrichedMetadata)
|
||||
}
|
||||
// 'literal' mode means no neural processing
|
||||
|
||||
// 🧠 AI Processing (Neural Import) - Based on processing mode
|
||||
if (shouldProcessNeurally) {
|
||||
try {
|
||||
// Execute augmentation pipeline for data processing
|
||||
// Note: Augmentations will be called via this.augmentations.execute during the actual add operation
|
||||
// This replaces the legacy SENSE pipeline
|
||||
|
||||
if (this.loggingConfig?.verbose) {
|
||||
console.log(`🧠 AI processing completed for data: ${id}`)
|
||||
}
|
||||
} catch (processingError) {
|
||||
// Don't fail the add operation if processing fails
|
||||
console.warn(`🧠 AI processing failed for ${id}:`, processingError)
|
||||
}
|
||||
}
|
||||
|
||||
return id
|
||||
} catch (error) {
|
||||
console.error('Failed to add vector:', error)
|
||||
|
||||
// Track error in health monitor
|
||||
if (this.monitoring) {
|
||||
this.monitoring.recordRequest(0, true)
|
||||
}
|
||||
|
||||
throw new Error(`Failed to add vector: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -7516,11 +7532,11 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
|
|||
}
|
||||
|
||||
// Store coordination plan in _system directory
|
||||
await this.addNoun({
|
||||
await this.addNoun('Cortex coordination plan', NounType.Process, {
|
||||
id: '_system/coordination',
|
||||
type: 'cortex_coordination',
|
||||
metadata: coordinationPlan
|
||||
})
|
||||
...coordinationPlan
|
||||
} as T)
|
||||
|
||||
prodLog.info('📋 Storage migration coordination plan created')
|
||||
prodLog.info('All services will automatically detect and execute the migration')
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@ export class BrainyChat {
|
|||
}
|
||||
}
|
||||
|
||||
// Store session using BrainyData add() method
|
||||
await this.brainy.add(
|
||||
// Store session using BrainyData addNoun() method
|
||||
await this.brainy.addNoun(
|
||||
{
|
||||
sessionType: 'chat',
|
||||
title: title || `Chat Session ${new Date().toLocaleDateString()}`,
|
||||
|
|
@ -120,9 +120,9 @@ export class BrainyChat {
|
|||
messageCount: session.messageCount,
|
||||
participants: session.participants
|
||||
},
|
||||
NounType.Concept, // Chat sessions are concepts
|
||||
{
|
||||
id: sessionId,
|
||||
nounType: NounType.Concept,
|
||||
sessionType: 'chat'
|
||||
}
|
||||
)
|
||||
|
|
@ -157,8 +157,8 @@ export class BrainyChat {
|
|||
metadata
|
||||
}
|
||||
|
||||
// Store message using BrainyData add() method
|
||||
await this.brainy.add(
|
||||
// Store message using BrainyData addNoun() method
|
||||
await this.brainy.addNoun(
|
||||
{
|
||||
messageType: 'chat',
|
||||
content,
|
||||
|
|
@ -167,9 +167,9 @@ export class BrainyChat {
|
|||
timestamp: timestamp.toISOString(),
|
||||
...metadata
|
||||
},
|
||||
NounType.Message, // Chat messages are Message type
|
||||
{
|
||||
id: messageId,
|
||||
nounType: NounType.Message,
|
||||
messageType: 'chat',
|
||||
sessionId: this.currentSessionId!,
|
||||
speaker
|
||||
|
|
@ -352,14 +352,14 @@ export class BrainyChat {
|
|||
|
||||
try {
|
||||
// Since BrainyData doesn't have update, add an archive marker
|
||||
await this.brainy.add(
|
||||
await this.brainy.addNoun(
|
||||
{
|
||||
archivedSessionId: sessionId,
|
||||
archivedAt: new Date().toISOString(),
|
||||
action: 'archive'
|
||||
},
|
||||
NounType.State, // Archive markers are State
|
||||
{
|
||||
nounType: NounType.State,
|
||||
sessionId,
|
||||
archived: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import chalk from 'chalk'
|
|||
import ora from 'ora'
|
||||
import { readFileSync, writeFileSync } from 'fs'
|
||||
import { BrainyData } from '../../brainyData.js'
|
||||
import { BrainyTypes, NounType, VerbType } from '../../index.js'
|
||||
|
||||
interface CoreOptions {
|
||||
verbose?: boolean
|
||||
|
|
@ -85,12 +86,36 @@ export const coreCommands = {
|
|||
metadata.id = options.id
|
||||
}
|
||||
|
||||
// Determine noun type
|
||||
let nounType: NounType
|
||||
if (options.type) {
|
||||
metadata.type = options.type
|
||||
// Validate provided type
|
||||
if (!BrainyTypes.isValidNoun(options.type)) {
|
||||
spinner.fail(`Invalid noun type: ${options.type}`)
|
||||
console.log(chalk.dim('Run "brainy types --noun" to see valid types'))
|
||||
process.exit(1)
|
||||
}
|
||||
nounType = options.type as NounType
|
||||
} else {
|
||||
// Use AI to suggest type
|
||||
spinner.text = 'Detecting type with AI...'
|
||||
const suggestion = await BrainyTypes.suggestNoun(
|
||||
typeof text === 'string' ? { content: text, ...metadata } : text
|
||||
)
|
||||
|
||||
if (suggestion.confidence < 0.6) {
|
||||
spinner.fail('Could not determine type with confidence')
|
||||
console.log(chalk.yellow(`Suggestion: ${suggestion.type} (${(suggestion.confidence * 100).toFixed(1)}%)`)))
|
||||
console.log(chalk.dim('Use --type flag to specify explicitly'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
nounType = suggestion.type as NounType
|
||||
spinner.text = `Using detected type: ${nounType}`
|
||||
}
|
||||
|
||||
// Smart detection by default
|
||||
const result = await brain.add(text, metadata)
|
||||
// Add with explicit type
|
||||
const result = await brain.addNoun(text, nounType, metadata)
|
||||
|
||||
spinner.succeed('Added successfully')
|
||||
|
||||
|
|
@ -312,13 +337,28 @@ export const coreCommands = {
|
|||
const batch = items.slice(i, i + batchSize)
|
||||
|
||||
for (const item of batch) {
|
||||
let content: string
|
||||
let metadata: any = {}
|
||||
|
||||
if (typeof item === 'string') {
|
||||
await brain.add(item)
|
||||
content = item
|
||||
} else if (item.content || item.text) {
|
||||
await brain.add(item.content || item.text, item.metadata || item)
|
||||
content = item.content || item.text
|
||||
metadata = item.metadata || item
|
||||
} else {
|
||||
await brain.add(JSON.stringify(item), { originalData: item })
|
||||
content = JSON.stringify(item)
|
||||
metadata = { originalData: item }
|
||||
}
|
||||
|
||||
// Use AI to detect type for each item
|
||||
const suggestion = await BrainyTypes.suggestNoun(
|
||||
typeof content === 'string' ? { content, ...metadata } : content
|
||||
)
|
||||
|
||||
// Use suggested type or default to Content if low confidence
|
||||
const nounType = suggestion.confidence >= 0.5 ? suggestion.type : NounType.Content
|
||||
|
||||
await brain.addNoun(content, nounType as NounType, metadata)
|
||||
imported++
|
||||
}
|
||||
|
||||
|
|
|
|||
226
src/cli/commands/types.ts
Normal file
226
src/cli/commands/types.ts
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
/**
|
||||
* CLI Commands for Type Management
|
||||
* Consistent with BrainyTypes public API
|
||||
*/
|
||||
|
||||
import chalk from 'chalk'
|
||||
import ora from 'ora'
|
||||
import inquirer from 'inquirer'
|
||||
import Table from 'cli-table3'
|
||||
import { BrainyTypes, NounType, VerbType } from '../../index.js'
|
||||
|
||||
/**
|
||||
* List types - matches BrainyTypes.nouns and BrainyTypes.verbs
|
||||
* Usage: brainy types
|
||||
*/
|
||||
export async function types(options: { json?: boolean, noun?: boolean, verb?: boolean }) {
|
||||
try {
|
||||
// Default to showing both if neither flag specified
|
||||
const showNouns = options.noun || (!options.noun && !options.verb)
|
||||
const showVerbs = options.verb || (!options.noun && !options.verb)
|
||||
|
||||
const result: any = {}
|
||||
if (showNouns) result.nouns = BrainyTypes.nouns
|
||||
if (showVerbs) result.verbs = BrainyTypes.verbs
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
return
|
||||
}
|
||||
|
||||
// Display nouns
|
||||
if (showNouns) {
|
||||
console.log(chalk.bold.cyan('\n📚 Noun Types (31):\n'))
|
||||
const nounChunks = []
|
||||
for (let i = 0; i < BrainyTypes.nouns.length; i += 3) {
|
||||
nounChunks.push(BrainyTypes.nouns.slice(i, i + 3))
|
||||
}
|
||||
|
||||
for (const chunk of nounChunks) {
|
||||
console.log(' ' + chunk.map(n => chalk.green(n.padEnd(20))).join(''))
|
||||
}
|
||||
}
|
||||
|
||||
// Display verbs
|
||||
if (showVerbs) {
|
||||
console.log(chalk.bold.cyan('\n🔗 Verb Types (40):\n'))
|
||||
const verbChunks = []
|
||||
for (let i = 0; i < BrainyTypes.verbs.length; i += 3) {
|
||||
verbChunks.push(BrainyTypes.verbs.slice(i, i + 3))
|
||||
}
|
||||
|
||||
for (const chunk of verbChunks) {
|
||||
console.log(' ' + chunk.map(v => chalk.blue(v.padEnd(20))).join(''))
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.dim('\n💡 Use "brainy suggest <data>" to get AI-powered type suggestions'))
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error:', error.message))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest type - matches BrainyTypes.suggestNoun() and suggestVerb()
|
||||
* Usage: brainy suggest <data>
|
||||
* Interactive if data not provided
|
||||
*/
|
||||
export async function suggest(
|
||||
data?: string,
|
||||
options: {
|
||||
verb?: boolean,
|
||||
json?: boolean
|
||||
} = {}
|
||||
) {
|
||||
try {
|
||||
// Interactive mode if no data provided
|
||||
if (!data) {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'kind',
|
||||
message: 'What type do you want to suggest?',
|
||||
choices: ['Noun', 'Verb'],
|
||||
default: 'Noun'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'data',
|
||||
message: 'Enter data (JSON or text):',
|
||||
validate: (input) => input.length > 0 || 'Data is required'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'hint',
|
||||
message: 'Relationship hint (optional):',
|
||||
when: (answers) => answers.kind === 'Verb'
|
||||
}
|
||||
])
|
||||
|
||||
data = answers.data
|
||||
options.verb = answers.kind === 'Verb'
|
||||
|
||||
// For verbs, parse source/target if provided as JSON
|
||||
if (options.verb && answers.hint) {
|
||||
data = JSON.stringify({ hint: answers.hint })
|
||||
}
|
||||
}
|
||||
|
||||
const spinner = ora('Analyzing with AI...').start()
|
||||
|
||||
let parsedData: any
|
||||
try {
|
||||
parsedData = JSON.parse(data)
|
||||
} catch {
|
||||
parsedData = { content: data }
|
||||
}
|
||||
|
||||
let suggestion
|
||||
if (options.verb) {
|
||||
// For verb suggestions, need source and target
|
||||
const source = parsedData.source || { type: 'unknown' }
|
||||
const target = parsedData.target || { type: 'unknown' }
|
||||
const hint = parsedData.hint || parsedData.relationship || parsedData.verb
|
||||
|
||||
suggestion = await BrainyTypes.suggestVerb(source, target, hint)
|
||||
spinner.succeed('Verb type analyzed')
|
||||
} else {
|
||||
suggestion = await BrainyTypes.suggestNoun(parsedData)
|
||||
spinner.succeed('Noun type analyzed')
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(suggestion, null, 2))
|
||||
return
|
||||
}
|
||||
|
||||
// Display results
|
||||
console.log(chalk.bold.green(`\n✨ Suggested: ${suggestion.type}`))
|
||||
console.log(chalk.cyan(`Confidence: ${(suggestion.confidence * 100).toFixed(1)}%`))
|
||||
|
||||
if (suggestion.alternatives && suggestion.alternatives.length > 0) {
|
||||
console.log(chalk.yellow('\nAlternatives:'))
|
||||
for (const alt of suggestion.alternatives.slice(0, 3)) {
|
||||
console.log(` ${alt.type} (${(alt.confidence * 100).toFixed(1)}%)`)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error:', error.message))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate type - matches BrainyTypes.isValidNoun() and isValidVerb()
|
||||
* Usage: brainy validate <type>
|
||||
*/
|
||||
export async function validate(
|
||||
type?: string,
|
||||
options: { verb?: boolean, json?: boolean } = {}
|
||||
) {
|
||||
try {
|
||||
// Interactive mode if no type provided
|
||||
if (!type) {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'kind',
|
||||
message: 'Validate as:',
|
||||
choices: ['Noun Type', 'Verb Type'],
|
||||
default: 'Noun Type'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'type',
|
||||
message: 'Enter type to validate:',
|
||||
validate: (input) => input.length > 0 || 'Type is required'
|
||||
}
|
||||
])
|
||||
|
||||
type = answers.type
|
||||
options.verb = answers.kind === 'Verb Type'
|
||||
}
|
||||
|
||||
const isValid = options.verb
|
||||
? BrainyTypes.isValidVerb(type)
|
||||
: BrainyTypes.isValidNoun(type)
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({
|
||||
type,
|
||||
kind: options.verb ? 'verb' : 'noun',
|
||||
valid: isValid
|
||||
}, null, 2))
|
||||
return
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
console.log(chalk.green(`✅ "${type}" is valid`))
|
||||
} else {
|
||||
console.log(chalk.red(`❌ "${type}" is invalid`))
|
||||
|
||||
// Show valid options
|
||||
const validTypes = options.verb ? BrainyTypes.verbs : BrainyTypes.nouns
|
||||
const similar = validTypes.filter(t =>
|
||||
t.toLowerCase().includes(type.toLowerCase()) ||
|
||||
type.toLowerCase().includes(t.toLowerCase())
|
||||
).slice(0, 5)
|
||||
|
||||
if (similar.length > 0) {
|
||||
console.log(chalk.yellow('\nDid you mean:'))
|
||||
similar.forEach(s => console.log(` ${s}`))
|
||||
} else {
|
||||
console.log(chalk.dim(`\nRun "brainy types" to see all valid types`))
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(isValid ? 0 : 1)
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('Error:', error.message))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,11 @@ async function runExample() {
|
|||
// Add vectors to the database
|
||||
const ids: Record<string, string> = {}
|
||||
for (const [word, vector] of Object.entries(wordEmbeddings)) {
|
||||
ids[word] = await db.addNoun(vector, metadata[word as keyof typeof metadata])
|
||||
// Determine noun type based on the metadata
|
||||
const meta = metadata[word as keyof typeof metadata]
|
||||
const nounType = meta.type === 'mammal' || meta.type === 'bird' || meta.type === 'fish' ? 'Thing' : 'Content'
|
||||
|
||||
ids[word] = await db.addNoun(vector, nounType, meta)
|
||||
|
||||
console.log(`Added "${word}" with ID: ${ids[word]}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
import { NounType, VerbType } from './types/graphTypes.js'
|
||||
import { NeuralImportAugmentation } from './augmentations/neuralImport.js'
|
||||
import { IntelligentTypeMatcher } from './augmentations/typeMatching/intelligentTypeMatcher.js'
|
||||
import { BrainyTypes } from './augmentations/typeMatching/brainyTypes.js'
|
||||
import * as fs from './universal/fs.js'
|
||||
import * as path from './universal/path.js'
|
||||
import { prodLog } from './utils/logger.js'
|
||||
|
|
@ -54,7 +54,7 @@ export interface ImportResult {
|
|||
|
||||
export class ImportManager {
|
||||
private neuralImport: NeuralImportAugmentation
|
||||
private typeMatcher: IntelligentTypeMatcher | null = null
|
||||
private typeMatcher: BrainyTypes | null = null
|
||||
private brain: any // BrainyData instance
|
||||
|
||||
constructor(brain: any) {
|
||||
|
|
@ -84,8 +84,8 @@ export class ImportManager {
|
|||
await this.neuralImport.initialize(context as any)
|
||||
|
||||
// Get type matcher
|
||||
const { getTypeMatcher } = await import('./augmentations/typeMatching/intelligentTypeMatcher.js')
|
||||
this.typeMatcher = await getTypeMatcher()
|
||||
const { getBrainyTypes } = await import('./augmentations/typeMatching/brainyTypes.js')
|
||||
this.typeMatcher = await getBrainyTypes()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
10
src/index.ts
10
src/index.ts
|
|
@ -450,15 +450,23 @@ export type {
|
|||
// Export type utility functions
|
||||
import { getNounTypes, getVerbTypes, getNounTypeMap, getVerbTypeMap } from './utils/typeUtils.js'
|
||||
|
||||
// Export BrainyTypes for complete type management
|
||||
import { BrainyTypes, TypeSuggestion, suggestType } from './utils/brainyTypes.js'
|
||||
|
||||
export {
|
||||
NounType,
|
||||
VerbType,
|
||||
getNounTypes,
|
||||
getVerbTypes,
|
||||
getNounTypeMap,
|
||||
getVerbTypeMap
|
||||
getVerbTypeMap,
|
||||
// BrainyTypes - complete type management
|
||||
BrainyTypes,
|
||||
suggestType
|
||||
}
|
||||
|
||||
export type { TypeSuggestion }
|
||||
|
||||
// Export MCP (Model Control Protocol) components
|
||||
import {
|
||||
BrainyMCPAdapter,
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export class BrainyMCPClient {
|
|||
// Store in Brainy for persistent memory
|
||||
if (this.brainy && message.type === 'message') {
|
||||
try {
|
||||
await this.brainy.add({
|
||||
await this.brainy.addNoun({
|
||||
text: `${message.from}: ${JSON.stringify(message.data)}`,
|
||||
metadata: {
|
||||
messageId: message.id,
|
||||
|
|
@ -132,7 +132,7 @@ export class BrainyMCPClient {
|
|||
type: message.type,
|
||||
event: message.event
|
||||
}
|
||||
})
|
||||
}, 'Message')
|
||||
} catch (error) {
|
||||
console.error('Error storing message in Brainy:', error)
|
||||
}
|
||||
|
|
@ -145,10 +145,10 @@ export class BrainyMCPClient {
|
|||
// Store history in Brainy
|
||||
if (this.brainy) {
|
||||
for (const histMsg of message.data.history) {
|
||||
await this.brainy.add({
|
||||
await this.brainy.addNoun({
|
||||
text: `${histMsg.from}: ${JSON.stringify(histMsg.data)}`,
|
||||
metadata: histMsg
|
||||
})
|
||||
}, 'Message')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -534,7 +534,7 @@ export class FileSystemStorage extends BaseStorage {
|
|||
/**
|
||||
* Save noun metadata to storage
|
||||
*/
|
||||
public async saveNounMetadata(id: string, metadata: any): Promise<void> {
|
||||
protected async saveNounMetadata_internal(id: string, metadata: any): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const filePath = path.join(this.nounMetadataDir, `${id}.json`)
|
||||
|
|
@ -562,7 +562,7 @@ export class FileSystemStorage extends BaseStorage {
|
|||
/**
|
||||
* Save verb metadata to storage
|
||||
*/
|
||||
public async saveVerbMetadata(id: string, metadata: any): Promise<void> {
|
||||
protected async saveVerbMetadata_internal(id: string, metadata: any): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const filePath = path.join(this.verbMetadataDir, `${id}.json`)
|
||||
|
|
|
|||
|
|
@ -531,9 +531,9 @@ export class MemoryStorage extends BaseStorage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Save noun metadata to storage
|
||||
* Save noun metadata to storage (internal implementation)
|
||||
*/
|
||||
public async saveNounMetadata(id: string, metadata: any): Promise<void> {
|
||||
protected async saveNounMetadata_internal(id: string, metadata: any): Promise<void> {
|
||||
this.nounMetadata.set(id, JSON.parse(JSON.stringify(metadata)))
|
||||
}
|
||||
|
||||
|
|
@ -550,9 +550,9 @@ export class MemoryStorage extends BaseStorage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Save verb metadata to storage
|
||||
* Save verb metadata to storage (internal implementation)
|
||||
*/
|
||||
public async saveVerbMetadata(id: string, metadata: any): Promise<void> {
|
||||
protected async saveVerbMetadata_internal(id: string, metadata: any): Promise<void> {
|
||||
this.verbMetadata.set(id, JSON.parse(JSON.stringify(metadata)))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -666,7 +666,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
/**
|
||||
* Save verb metadata to storage
|
||||
*/
|
||||
public async saveVerbMetadata(id: string, metadata: any): Promise<void> {
|
||||
protected async saveVerbMetadata_internal(id: string, metadata: any): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const fileName = `${id}.json`
|
||||
|
|
@ -703,7 +703,7 @@ export class OPFSStorage extends BaseStorage {
|
|||
/**
|
||||
* Save noun metadata to storage
|
||||
*/
|
||||
public async saveNounMetadata(id: string, metadata: any): Promise<void> {
|
||||
protected async saveNounMetadata_internal(id: string, metadata: any): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const fileName = `${id}.json`
|
||||
|
|
|
|||
|
|
@ -1880,7 +1880,7 @@ export class S3CompatibleStorage extends BaseStorage {
|
|||
/**
|
||||
* Save verb metadata to storage
|
||||
*/
|
||||
public async saveVerbMetadata(id: string, metadata: any): Promise<void> {
|
||||
protected async saveVerbMetadata_internal(id: string, metadata: any): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
try {
|
||||
|
|
@ -1970,7 +1970,7 @@ export class S3CompatibleStorage extends BaseStorage {
|
|||
/**
|
||||
* Save noun metadata to storage
|
||||
*/
|
||||
public async saveNounMetadata(id: string, metadata: any): Promise<void> {
|
||||
protected async saveNounMetadata_internal(id: string, metadata: any): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
import { GraphVerb, HNSWNoun, HNSWVerb, StatisticsData } from '../coreTypes.js'
|
||||
import { BaseStorageAdapter } from './adapters/baseStorageAdapter.js'
|
||||
import { validateNounType, validateVerbType } from '../utils/typeValidation.js'
|
||||
import { NounType, VerbType } from '../types/graphTypes.js'
|
||||
|
||||
// Common directory/prefix names
|
||||
// Option A: Entity-Based Directory Structure
|
||||
|
|
@ -81,6 +83,11 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
*/
|
||||
public async saveNoun(noun: HNSWNoun): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
// Validate noun type before saving - storage boundary protection
|
||||
const metadata = await this.getNounMetadata(noun.id)
|
||||
if (metadata?.noun) {
|
||||
validateNounType(metadata.noun)
|
||||
}
|
||||
return this.saveNoun_internal(noun)
|
||||
}
|
||||
|
||||
|
|
@ -116,6 +123,11 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
public async saveVerb(verb: GraphVerb): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Validate verb type before saving - storage boundary protection
|
||||
if (verb.verb) {
|
||||
validateVerbType(verb.verb)
|
||||
}
|
||||
|
||||
// Extract the lightweight HNSWVerb data
|
||||
const hnswVerb: HNSWVerb = {
|
||||
id: verb.id,
|
||||
|
|
@ -635,7 +647,19 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
* Save noun metadata to storage
|
||||
* This method should be implemented by each specific adapter
|
||||
*/
|
||||
public abstract saveNounMetadata(id: string, metadata: any): Promise<void>
|
||||
public async saveNounMetadata(id: string, metadata: any): Promise<void> {
|
||||
// Validate noun type in metadata - storage boundary protection
|
||||
if (metadata?.noun) {
|
||||
validateNounType(metadata.noun)
|
||||
}
|
||||
return this.saveNounMetadata_internal(id, metadata)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method for saving noun metadata
|
||||
* This method should be implemented by each specific adapter
|
||||
*/
|
||||
protected abstract saveNounMetadata_internal(id: string, metadata: any): Promise<void>
|
||||
|
||||
/**
|
||||
* Get noun metadata from storage
|
||||
|
|
@ -647,7 +671,19 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
* Save verb metadata to storage
|
||||
* This method should be implemented by each specific adapter
|
||||
*/
|
||||
public abstract saveVerbMetadata(id: string, metadata: any): Promise<void>
|
||||
public async saveVerbMetadata(id: string, metadata: any): Promise<void> {
|
||||
// Validate verb type in metadata - storage boundary protection
|
||||
if (metadata?.verb) {
|
||||
validateVerbType(metadata.verb)
|
||||
}
|
||||
return this.saveVerbMetadata_internal(id, metadata)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method for saving verb metadata
|
||||
* This method should be implemented by each specific adapter
|
||||
*/
|
||||
protected abstract saveVerbMetadata_internal(id: string, metadata: any): Promise<void>
|
||||
|
||||
/**
|
||||
* Get verb metadata from storage
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ export interface BrainyDataInterface<T = unknown> {
|
|||
/**
|
||||
* Add a noun (entity with vector and metadata) to the database
|
||||
* @param data Text string or vector representation (will auto-embed strings)
|
||||
* @param nounType Required noun type (one of 31 types)
|
||||
* @param metadata Optional metadata to associate with the noun
|
||||
* @param options Optional configuration including custom ID
|
||||
* @returns The ID of the added noun
|
||||
*/
|
||||
addNoun(data: string | Vector, metadata?: T, options?: { id?: string; [key: string]: any }): Promise<string>
|
||||
addNoun(data: string | Vector, nounType: string, metadata?: T): Promise<string>
|
||||
|
||||
/**
|
||||
* Search for text in the database
|
||||
|
|
|
|||
326
src/utils/brainyTypes.ts
Normal file
326
src/utils/brainyTypes.ts
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
/**
|
||||
* BrainyTypes - Complete type management for Brainy
|
||||
*
|
||||
* Provides type lists, validation, and intelligent suggestions
|
||||
* for nouns and verbs using semantic embeddings.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { BrainyTypes } from '@soulcraft/brainy'
|
||||
*
|
||||
* // Get all available types
|
||||
* const nounTypes = BrainyTypes.nouns // ['Person', 'Organization', ...]
|
||||
* const verbTypes = BrainyTypes.verbs // ['Contains', 'Creates', ...]
|
||||
*
|
||||
* // Validate types
|
||||
* BrainyTypes.isValidNoun('Person') // true
|
||||
* BrainyTypes.isValidVerb('Unknown') // false
|
||||
*
|
||||
* // Get intelligent suggestions
|
||||
* const personData = {
|
||||
* name: 'John Doe',
|
||||
* email: 'john@example.com'
|
||||
* }
|
||||
* const suggestion = await BrainyTypes.suggestNoun(personData)
|
||||
* console.log(suggestion.type) // 'Person'
|
||||
* console.log(suggestion.confidence) // 0.92
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { NounType, VerbType } from '../types/graphTypes.js'
|
||||
import { BrainyTypes as InternalBrainyTypes, TypeMatchResult } from '../augmentations/typeMatching/brainyTypes.js'
|
||||
|
||||
/**
|
||||
* Type suggestion result
|
||||
*/
|
||||
export interface TypeSuggestion {
|
||||
/** The suggested type */
|
||||
type: NounType | VerbType
|
||||
/** Confidence score between 0 and 1 */
|
||||
confidence: number
|
||||
/** Human-readable explanation */
|
||||
reason?: string
|
||||
/** Alternative suggestions */
|
||||
alternatives?: Array<{
|
||||
type: NounType | VerbType
|
||||
confidence: number
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* BrainyTypes - Complete type management for Brainy
|
||||
*
|
||||
* Static class providing type lists, validation, and intelligent suggestions.
|
||||
* No instantiation needed - all methods are static.
|
||||
*/
|
||||
export class BrainyTypes {
|
||||
private static instance: InternalBrainyTypes | null = null
|
||||
private static initialized = false
|
||||
|
||||
/**
|
||||
* All available noun types
|
||||
* @example
|
||||
* ```typescript
|
||||
* BrainyTypes.nouns.forEach(type => console.log(type))
|
||||
* // 'Person', 'Organization', 'Location', ...
|
||||
* ```
|
||||
*/
|
||||
static readonly nouns: readonly NounType[] = Object.freeze(Object.values(NounType))
|
||||
|
||||
/**
|
||||
* All available verb types
|
||||
* @example
|
||||
* ```typescript
|
||||
* BrainyTypes.verbs.forEach(type => console.log(type))
|
||||
* // 'Contains', 'Creates', 'RelatedTo', ...
|
||||
* ```
|
||||
*/
|
||||
static readonly verbs: readonly VerbType[] = Object.freeze(Object.values(VerbType))
|
||||
|
||||
/**
|
||||
* Get or create the internal matcher instance
|
||||
*/
|
||||
private static async getInternalMatcher(): Promise<InternalBrainyTypes> {
|
||||
if (!this.instance) {
|
||||
this.instance = new InternalBrainyTypes()
|
||||
await this.instance.init()
|
||||
this.initialized = true
|
||||
}
|
||||
return this.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid noun type
|
||||
*
|
||||
* @param type The type string to check
|
||||
* @returns True if valid noun type
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* BrainyTypes.isValidNoun('Person') // true
|
||||
* BrainyTypes.isValidNoun('Unknown') // false
|
||||
* BrainyTypes.isValidNoun('Contains') // false (it's a verb)
|
||||
* ```
|
||||
*/
|
||||
static isValidNoun(type: string): type is NounType {
|
||||
return (this.nouns as readonly string[]).includes(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid verb type
|
||||
*
|
||||
* @param type The type string to check
|
||||
* @returns True if valid verb type
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* BrainyTypes.isValidVerb('Contains') // true
|
||||
* BrainyTypes.isValidVerb('Unknown') // false
|
||||
* BrainyTypes.isValidVerb('Person') // false (it's a noun)
|
||||
* ```
|
||||
*/
|
||||
static isValidVerb(type: string): type is VerbType {
|
||||
return (this.verbs as readonly string[]).includes(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest the most appropriate noun type for an object
|
||||
*
|
||||
* @param data The object or data to analyze
|
||||
* @returns Promise resolving to type suggestion with confidence score
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const data = {
|
||||
* title: 'Quarterly Report',
|
||||
* author: 'Jane Smith',
|
||||
* pages: 42
|
||||
* }
|
||||
* const suggestion = await BrainyTypes.suggestNoun(data)
|
||||
* console.log(suggestion.type) // 'Document'
|
||||
* console.log(suggestion.confidence) // 0.88
|
||||
*
|
||||
* // Check alternatives if confidence is low
|
||||
* if (suggestion.confidence < 0.8) {
|
||||
* console.log('Also consider:', suggestion.alternatives)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
static async suggestNoun(data: any): Promise<TypeSuggestion> {
|
||||
const matcher = await this.getInternalMatcher()
|
||||
const result = await matcher.matchNounType(data)
|
||||
|
||||
return {
|
||||
type: result.type as NounType,
|
||||
confidence: result.confidence,
|
||||
reason: result.reasoning,
|
||||
alternatives: result.alternatives?.map(alt => ({
|
||||
type: alt.type as NounType,
|
||||
confidence: alt.confidence
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest the most appropriate verb type for a relationship
|
||||
*
|
||||
* @param source The source entity
|
||||
* @param target The target entity
|
||||
* @param hint Optional hint about the relationship
|
||||
* @returns Promise resolving to type suggestion with confidence score
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const source = { type: 'Person', name: 'Alice' }
|
||||
* const target = { type: 'Document', title: 'Research Paper' }
|
||||
*
|
||||
* const suggestion = await BrainyTypes.suggestVerb(source, target, 'authored')
|
||||
* console.log(suggestion.type) // 'CreatedBy'
|
||||
* console.log(suggestion.confidence) // 0.91
|
||||
*
|
||||
* // Without hint
|
||||
* const suggestion2 = await BrainyTypes.suggestVerb(source, target)
|
||||
* console.log(suggestion2.type) // 'RelatedTo' (more generic)
|
||||
* ```
|
||||
*/
|
||||
static async suggestVerb(
|
||||
source: any,
|
||||
target: any,
|
||||
hint?: string
|
||||
): Promise<TypeSuggestion> {
|
||||
const matcher = await this.getInternalMatcher()
|
||||
const result = await matcher.matchVerbType(source, target, hint)
|
||||
|
||||
return {
|
||||
type: result.type as VerbType,
|
||||
confidence: result.confidence,
|
||||
reason: result.reasoning,
|
||||
alternatives: result.alternatives?.map(alt => ({
|
||||
type: alt.type as VerbType,
|
||||
confidence: alt.confidence
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a noun type by name (with validation)
|
||||
*
|
||||
* @param name The noun type name
|
||||
* @returns The NounType enum value
|
||||
* @throws Error if invalid noun type
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const type = BrainyTypes.getNoun('Person') // NounType.Person
|
||||
* const bad = BrainyTypes.getNoun('Unknown') // throws Error
|
||||
* ```
|
||||
*/
|
||||
static getNoun(name: string): NounType {
|
||||
if (!this.isValidNoun(name)) {
|
||||
throw new Error(`Invalid noun type: '${name}'. Valid types are: ${this.nouns.join(', ')}`)
|
||||
}
|
||||
return name as NounType
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a verb type by name (with validation)
|
||||
*
|
||||
* @param name The verb type name
|
||||
* @returns The VerbType enum value
|
||||
* @throws Error if invalid verb type
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const type = BrainyTypes.getVerb('Contains') // VerbType.Contains
|
||||
* const bad = BrainyTypes.getVerb('Unknown') // throws Error
|
||||
* ```
|
||||
*/
|
||||
static getVerb(name: string): VerbType {
|
||||
if (!this.isValidVerb(name)) {
|
||||
throw new Error(`Invalid verb type: '${name}'. Valid types are: ${this.verbs.join(', ')}`)
|
||||
}
|
||||
return name as VerbType
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the internal cache
|
||||
* Useful when processing many different types of data
|
||||
*/
|
||||
static clearCache(): void {
|
||||
this.instance?.clearCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of resources
|
||||
* Call when completely done using BrainyTypes
|
||||
*/
|
||||
static async dispose(): Promise<void> {
|
||||
if (this.instance) {
|
||||
await this.instance.dispose()
|
||||
this.instance = null
|
||||
this.initialized = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get noun types as a plain object (for iteration)
|
||||
* @returns Object with noun type names as keys
|
||||
*/
|
||||
static getNounMap(): Record<string, NounType> {
|
||||
const map: Record<string, NounType> = {}
|
||||
for (const noun of this.nouns) {
|
||||
map[noun] = noun
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Get verb types as a plain object (for iteration)
|
||||
* @returns Object with verb type names as keys
|
||||
*/
|
||||
static getVerbMap(): Record<string, VerbType> {
|
||||
const map: Record<string, VerbType> = {}
|
||||
for (const verb of this.verbs) {
|
||||
map[verb] = verb
|
||||
}
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export the enums for convenience
|
||||
export { NounType, VerbType }
|
||||
|
||||
/**
|
||||
* Helper function to validate and suggest types in one call
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { suggestType } from '@soulcraft/brainy'
|
||||
*
|
||||
* // For nouns
|
||||
* const nounSuggestion = await suggestType('noun', data)
|
||||
*
|
||||
* // For verbs
|
||||
* const verbSuggestion = await suggestType('verb', source, target)
|
||||
* ```
|
||||
*/
|
||||
export async function suggestType(
|
||||
kind: 'noun',
|
||||
data: any
|
||||
): Promise<TypeSuggestion>
|
||||
export async function suggestType(
|
||||
kind: 'verb',
|
||||
source: any,
|
||||
target: any,
|
||||
hint?: string
|
||||
): Promise<TypeSuggestion>
|
||||
export async function suggestType(
|
||||
kind: 'noun' | 'verb',
|
||||
...args: any[]
|
||||
): Promise<TypeSuggestion> {
|
||||
if (kind === 'noun') {
|
||||
return BrainyTypes.suggestNoun(args[0])
|
||||
} else {
|
||||
return BrainyTypes.suggestVerb(args[0], args[1], args[2])
|
||||
}
|
||||
}
|
||||
173
src/utils/typeValidation.ts
Normal file
173
src/utils/typeValidation.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { NounType, VerbType } from '../types/graphTypes.js'
|
||||
|
||||
// Type sets for O(1) validation
|
||||
const VALID_NOUN_TYPES = new Set<string>(Object.values(NounType))
|
||||
const VALID_VERB_TYPES = new Set<string>(Object.values(VerbType))
|
||||
|
||||
// Type guards
|
||||
export function isValidNounType(type: unknown): type is NounType {
|
||||
return typeof type === 'string' && VALID_NOUN_TYPES.has(type as string)
|
||||
}
|
||||
|
||||
export function isValidVerbType(type: unknown): type is VerbType {
|
||||
return typeof type === 'string' && VALID_VERB_TYPES.has(type as string)
|
||||
}
|
||||
|
||||
// Validators with helpful errors
|
||||
export function validateNounType(type: unknown): NounType {
|
||||
if (!isValidNounType(type)) {
|
||||
const suggestion = findClosestMatch(String(type), VALID_NOUN_TYPES)
|
||||
throw new Error(
|
||||
`Invalid noun type: '${type}'. ${suggestion ? `Did you mean '${suggestion}'?` : ''} ` +
|
||||
`Valid types are: ${[...VALID_NOUN_TYPES].sort().join(', ')}`
|
||||
)
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
export function validateVerbType(type: unknown): VerbType {
|
||||
if (!isValidVerbType(type)) {
|
||||
const suggestion = findClosestMatch(String(type), VALID_VERB_TYPES)
|
||||
throw new Error(
|
||||
`Invalid verb type: '${type}'. ${suggestion ? `Did you mean '${suggestion}'?` : ''} ` +
|
||||
`Valid types are: ${[...VALID_VERB_TYPES].sort().join(', ')}`
|
||||
)
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
// Graph entity validators
|
||||
export interface ValidatedGraphNoun {
|
||||
noun: NounType
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ValidatedGraphVerb {
|
||||
verb: VerbType
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export function validateGraphNoun(noun: unknown): ValidatedGraphNoun {
|
||||
if (!noun || typeof noun !== 'object') {
|
||||
throw new Error('Invalid noun: must be an object')
|
||||
}
|
||||
const n = noun as any
|
||||
if (!n.noun) {
|
||||
throw new Error('Invalid noun: missing required "noun" type field')
|
||||
}
|
||||
n.noun = validateNounType(n.noun)
|
||||
return n as ValidatedGraphNoun
|
||||
}
|
||||
|
||||
export function validateGraphVerb(verb: unknown): ValidatedGraphVerb {
|
||||
if (!verb || typeof verb !== 'object') {
|
||||
throw new Error('Invalid verb: must be an object')
|
||||
}
|
||||
const v = verb as any
|
||||
if (!v.verb) {
|
||||
throw new Error('Invalid verb: missing required "verb" type field')
|
||||
}
|
||||
v.verb = validateVerbType(v.verb)
|
||||
return v as ValidatedGraphVerb
|
||||
}
|
||||
|
||||
// Helper for suggestions using Levenshtein distance
|
||||
function findClosestMatch(input: string, validSet: Set<string>): string | null {
|
||||
if (!input) return null
|
||||
|
||||
const lower = input.toLowerCase()
|
||||
let bestMatch: string | null = null
|
||||
let bestScore = Infinity
|
||||
|
||||
for (const valid of validSet) {
|
||||
const validLower = valid.toLowerCase()
|
||||
|
||||
// Exact match (case-insensitive)
|
||||
if (validLower === lower) {
|
||||
return valid
|
||||
}
|
||||
|
||||
// Substring match
|
||||
if (validLower.includes(lower) || lower.includes(validLower)) {
|
||||
return valid
|
||||
}
|
||||
|
||||
// Calculate Levenshtein distance
|
||||
const distance = levenshteinDistance(lower, validLower)
|
||||
if (distance < bestScore && distance <= 3) { // Threshold of 3 for suggestions
|
||||
bestScore = distance
|
||||
bestMatch = valid
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch
|
||||
}
|
||||
|
||||
// Levenshtein distance implementation
|
||||
function levenshteinDistance(str1: string, str2: string): number {
|
||||
const m = str1.length
|
||||
const n = str2.length
|
||||
const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0))
|
||||
|
||||
for (let i = 0; i <= m; i++) {
|
||||
dp[i][0] = i
|
||||
}
|
||||
|
||||
for (let j = 0; j <= n; j++) {
|
||||
dp[0][j] = j
|
||||
}
|
||||
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (str1[i - 1] === str2[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
} else {
|
||||
dp[i][j] = 1 + Math.min(
|
||||
dp[i - 1][j], // deletion
|
||||
dp[i][j - 1], // insertion
|
||||
dp[i - 1][j - 1] // substitution
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dp[m][n]
|
||||
}
|
||||
|
||||
// Batch validation helpers
|
||||
export function validateNounTypes(types: unknown[]): NounType[] {
|
||||
return types.map(validateNounType)
|
||||
}
|
||||
|
||||
export function validateVerbTypes(types: unknown[]): VerbType[] {
|
||||
return types.map(validateVerbType)
|
||||
}
|
||||
|
||||
|
||||
// Export validation statistics for monitoring
|
||||
export interface ValidationStats {
|
||||
validated: number
|
||||
failed: number
|
||||
inferred: number
|
||||
suggestions: number
|
||||
}
|
||||
|
||||
let stats: ValidationStats = {
|
||||
validated: 0,
|
||||
failed: 0,
|
||||
inferred: 0,
|
||||
suggestions: 0
|
||||
}
|
||||
|
||||
export function getValidationStats(): ValidationStats {
|
||||
return { ...stats }
|
||||
}
|
||||
|
||||
export function resetValidationStats(): void {
|
||||
stats = {
|
||||
validated: 0,
|
||||
failed: 0,
|
||||
inferred: 0,
|
||||
suggestions: 0
|
||||
}
|
||||
}
|
||||
20
test-strict-default.js
Normal file
20
test-strict-default.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Quick test: Verify strict mode is default
|
||||
import { BrainyData } from './dist/brainyData.js'
|
||||
|
||||
// Test 1: Default config should be strict
|
||||
const brain1 = new BrainyData({})
|
||||
console.log('Test 1 - Default is strict:', brain1.typeCompatibilityMode === false ? '✅ PASS' : '❌ FAIL')
|
||||
|
||||
// Test 2: Empty config should be strict
|
||||
const brain2 = new BrainyData()
|
||||
console.log('Test 2 - No config is strict:', brain2.typeCompatibilityMode === false ? '✅ PASS' : '❌ FAIL')
|
||||
|
||||
// Test 3: Explicit false should be strict
|
||||
const brain3 = new BrainyData({ typeCompatibilityMode: false })
|
||||
console.log('Test 3 - Explicit false is strict:', brain3.typeCompatibilityMode === false ? '✅ PASS' : '❌ FAIL')
|
||||
|
||||
// Test 4: Only true enables compatibility
|
||||
const brain4 = new BrainyData({ typeCompatibilityMode: true })
|
||||
console.log('Test 4 - True enables compat:', brain4.typeCompatibilityMode === true ? '✅ PASS' : '❌ FAIL')
|
||||
|
||||
console.log('\n✨ Summary: Strict mode is now the default!')
|
||||
103
tests/type-enforcement.test.ts
Normal file
103
tests/type-enforcement.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { BrainyData } from '../src/brainyData.js'
|
||||
import { NounType, VerbType } from '../src/types/graphTypes.js'
|
||||
import { validateNounType, validateVerbType } from '../src/utils/typeValidation.js'
|
||||
|
||||
describe('Type Enforcement System', () => {
|
||||
describe('Type Validation Module', () => {
|
||||
it('should validate correct noun types', () => {
|
||||
expect(validateNounType(NounType.Person)).toBe('person')
|
||||
expect(validateNounType('organization')).toBe('organization')
|
||||
expect(validateNounType(NounType.Document)).toBe('document')
|
||||
})
|
||||
|
||||
it('should reject invalid noun types with helpful errors', () => {
|
||||
expect(() => validateNounType('invalid')).toThrow('Invalid noun type')
|
||||
expect(() => validateNounType('')).toThrow('Invalid noun type')
|
||||
expect(() => validateNounType(null)).toThrow('Invalid noun type')
|
||||
expect(() => validateNounType(undefined)).toThrow('Invalid noun type')
|
||||
})
|
||||
|
||||
it('should provide helpful suggestions for typos', () => {
|
||||
expect(() => validateNounType('persan')).toThrow(/Did you mean 'person'/)
|
||||
expect(() => validateNounType('doc')).toThrow(/Did you mean 'document'/)
|
||||
expect(() => validateNounType('org')).toThrow(/Did you mean 'organization'/)
|
||||
})
|
||||
|
||||
it('should validate correct verb types', () => {
|
||||
expect(validateVerbType(VerbType.RelatedTo)).toBe('relatedTo')
|
||||
expect(validateVerbType('contains')).toBe('contains')
|
||||
expect(validateVerbType(VerbType.Creates)).toBe('creates')
|
||||
})
|
||||
})
|
||||
|
||||
describe('addNoun with Type Enforcement', () => {
|
||||
let brain: BrainyData
|
||||
|
||||
beforeEach(async () => {
|
||||
brain = new BrainyData({
|
||||
storage: { forceMemoryStorage: true },
|
||||
logging: { verbose: false }
|
||||
})
|
||||
await brain.init()
|
||||
})
|
||||
|
||||
it('should require noun type parameter', async () => {
|
||||
// TypeScript should prevent this, but test runtime validation
|
||||
await expect(
|
||||
(brain.addNoun as any)('Test data', { title: 'Test' })
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should accept valid noun type', async () => {
|
||||
const id = await brain.addNoun('John Doe', NounType.Person, { age: 30 })
|
||||
expect(id).toBeDefined()
|
||||
|
||||
const noun = await brain.getNoun(id)
|
||||
expect(noun.metadata.noun).toBe('person')
|
||||
expect(noun.metadata.age).toBe(30)
|
||||
})
|
||||
|
||||
it('should accept noun type as string', async () => {
|
||||
const id = await brain.addNoun('Document content', 'document', {
|
||||
title: 'Important Doc',
|
||||
author: 'Jane'
|
||||
})
|
||||
expect(id).toBeDefined()
|
||||
|
||||
const noun = await brain.getNoun(id)
|
||||
expect(noun.metadata.noun).toBe('document')
|
||||
expect(noun.metadata.title).toBe('Important Doc')
|
||||
})
|
||||
|
||||
it('should validate noun type and reject invalid ones', async () => {
|
||||
await expect(
|
||||
brain.addNoun('Test', 'invalidType', {})
|
||||
).rejects.toThrow('Invalid noun type')
|
||||
})
|
||||
|
||||
it('should provide suggestions for typos', async () => {
|
||||
await expect(
|
||||
brain.addNoun('Test', 'persan', {})
|
||||
).rejects.toThrow(/Did you mean 'person'/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Type Validation Performance', () => {
|
||||
it('should validate types quickly (O(1) with Set)', () => {
|
||||
const start = performance.now()
|
||||
|
||||
// Validate 10000 types
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
validateNounType(NounType.Person)
|
||||
validateVerbType(VerbType.Contains)
|
||||
}
|
||||
|
||||
const end = performance.now()
|
||||
const duration = end - start
|
||||
|
||||
// Should complete in less than 50ms (very generous, usually < 5ms)
|
||||
expect(duration).toBeLessThan(50)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue