feat: add confidence/weight to Entity and flatten Result fields for convenient access

Add confidence and weight properties to Entity interface and flatten Result fields to top level for improved developer experience and API consistency.

Breaking Changes: None (all changes are backward compatible)

Phase 2 - Entity Confidence & Weight:
- Add confidence (type classification certainty) and weight (entity importance) to Entity interface
- Add confidence/weight parameters to AddParams and UpdateParams
- Update convertNounToEntity() to extract confidence/weight from storage
- Update add() and update() methods to preserve confidence/weight in metadata
- Enable developers to specify and access entity confidence/weight scores

Phase 3 - Result Field Flattening:
- Flatten commonly-used entity fields (type, metadata, data, confidence, weight) to Result top level
- Add createResult() helper for consistent Result construction
- Update all find() code paths to use createResult()
- Enable direct access: result.metadata instead of result.entity.metadata
- Preserve full entity in result.entity for backward compatibility

VFS Fix (from previous work):
- Fix VFSStructureGenerator to use brain.vfs() cached instance instead of creating separate instance
- Improve VFS error messages with step-by-step guidance
- Update examples to show correct vfs.init() usage
- Add comprehensive VFS import verification tests

Documentation Updates:
- Update API_REFERENCE.md with confidence/weight examples and flattened Result documentation
- Enhance JSDoc for add(), get(), find(), similar() with v4.3.0 examples
- Document Result structure changes and backward compatibility
- Add migration examples showing both old and new access patterns

Tests:
- Add 16 comprehensive tests for Entity confidence/weight exposure
- Add tests for Result field flattening
- Add tests for backward compatibility
- All tests passing (16/16)

API Consistency:
- Entity: direct access to confidence/weight
- Result: flattened fields + nested entity (both work)
- Relation: already had confidence/weight (consistent)
- VFS: inherits from Entity (automatic)

Files Changed:
- src/types/brainy.types.ts - Updated Entity, AddParams, UpdateParams, Result interfaces
- src/brainy.ts - Updated implementation and JSDoc for all affected methods
- tests/integration/entity-confidence-weight.test.ts - 16 comprehensive tests
- docs/API_REFERENCE.md - Updated with v4.3.0 examples
- src/importers/VFSStructureGenerator.ts - VFS fix
- src/vfs/VirtualFileSystem.ts - Improved error messages
- examples/unified-import-example.ts - Added vfs.init() example
- tests/integration/vfs-*-verification.test.ts - VFS verification tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-10-23 12:19:50 -07:00
parent 6d4046fbd8
commit 4f22c46f4c
9 changed files with 982 additions and 53 deletions

View file

@ -48,7 +48,8 @@ import {
DeleteManyParams,
RelateManyParams,
BatchResult,
BrainyConfig
BrainyConfig,
ScoreExplanation
} from './types/brainy.types.js'
import { NounType, VerbType } from './types/graphTypes.js'
import { BrainyInterface } from './types/brainyInterface.js'
@ -296,6 +297,14 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* Add an entity to the database
*
* @param params - Parameters for adding the entity
* @param params.data - Content to embed and store (required)
* @param params.type - NounType classification (required)
* @param params.metadata - Custom metadata object
* @param params.id - Custom ID (auto-generated if not provided)
* @param params.vector - Pre-computed embedding vector
* @param params.service - Service name for multi-tenancy
* @param params.confidence - Type classification confidence (0-1) *New in v4.3.0*
* @param params.weight - Entity importance/salience (0-1) *New in v4.3.0*
* @returns Promise that resolves to the entity ID
*
* @example Basic entity creation
@ -308,6 +317,17 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* console.log(`Created entity: ${id}`)
* ```
*
* @example Adding with confidence and weight (New in v4.3.0)
* ```typescript
* const id = await brain.add({
* data: "Machine learning model for sentiment analysis",
* type: NounType.Concept,
* metadata: { accuracy: 0.95, version: "2.1" },
* confidence: 0.92, // High confidence in Concept classification
* weight: 0.85 // High importance entity
* })
* ```
*
* @example Adding with custom ID
* ```typescript
* const customId = await brain.add({
@ -377,7 +397,10 @@ export class Brainy<T = any> implements BrainyInterface<T> {
_data: params.data, // Store the raw data in metadata
noun: params.type,
service: params.service,
createdAt: Date.now()
createdAt: Date.now(),
// Preserve confidence and weight if provided
...(params.confidence !== undefined && { confidence: params.confidence }),
...(params.weight !== undefined && { weight: params.weight })
}
// v4.0.0: Save vector and metadata separately
@ -403,6 +426,11 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* @param id - The unique identifier of the entity to retrieve
* @returns Promise that resolves to the entity if found, null if not found
*
* **Entity includes (v4.3.0):**
* - `confidence` - Type classification confidence (0-1) if set
* - `weight` - Entity importance/salience (0-1) if set
* - All standard fields: id, type, data, metadata, vector, timestamps
*
* @example
* // Basic entity retrieval
* const entity = await brainy.get('user-123')
@ -414,6 +442,15 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* }
*
* @example
* // Accessing confidence and weight (New in v4.3.0)
* const entity = await brainy.get('concept-456')
* if (entity) {
* console.log(`Type: ${entity.type}`)
* console.log(`Confidence: ${entity.confidence ?? 'N/A'}`)
* console.log(`Weight: ${entity.weight ?? 'N/A'}`)
* }
*
* @example
* // Working with typed entities
* interface User {
* name: string
@ -483,13 +520,43 @@ export class Brainy<T = any> implements BrainyInterface<T> {
})
}
/**
* Create a flattened Result object from entity
* Flattens commonly-used entity fields to top level for convenience
*/
private createResult(id: string, score: number, entity: Entity<T>, explanation?: ScoreExplanation): Result<T> {
return {
id,
score,
// Flatten common entity fields to top level
type: entity.type,
metadata: entity.metadata,
data: entity.data,
confidence: entity.confidence,
weight: entity.weight,
// Preserve full entity for backward compatibility
entity,
// Optional score explanation
...(explanation && { explanation })
}
}
/**
* Convert a noun from storage to an entity
*/
private async convertNounToEntity(noun: any): Promise<Entity<T>> {
// Extract metadata - separate user metadata from system metadata
const { noun: nounType, service, createdAt, updatedAt, _data, ...userMetadata } = noun.metadata || {}
const {
noun: nounType,
service,
createdAt,
updatedAt,
_data,
confidence, // Entity confidence score (0-1)
weight, // Entity importance/salience (0-1)
...userMetadata
} = noun.metadata || {}
const entity: Entity<T> = {
id: noun.id,
vector: noun.vector,
@ -499,12 +566,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
createdAt: (createdAt as number) || Date.now(),
updatedAt: updatedAt as number
}
// Only add data field if it exists
// Only add optional fields if they exist
if (_data !== undefined) {
entity.data = _data
}
if (confidence !== undefined) {
entity.confidence = confidence as number
}
if (weight !== undefined) {
entity.weight = weight as number
}
return entity
}
@ -558,7 +631,12 @@ export class Brainy<T = any> implements BrainyInterface<T> {
noun: params.type || existing.type,
service: existing.service,
createdAt: existing.createdAt,
updatedAt: Date.now()
updatedAt: Date.now(),
// Update confidence and weight if provided, otherwise preserve existing
...(params.confidence !== undefined && { confidence: params.confidence }),
...(params.weight !== undefined && { weight: params.weight }),
...(params.confidence === undefined && existing.confidence !== undefined && { confidence: existing.confidence }),
...(params.weight === undefined && existing.weight !== undefined && { weight: existing.weight })
}
// v4.0.0: Save vector and metadata separately
@ -961,6 +1039,13 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* @param query - Natural language string or structured FindParams object
* @returns Promise that resolves to array of search results with scores
*
* **Result Structure (v4.3.0):**
* Each result includes flattened entity fields for convenient access:
* - `metadata`, `type`, `data` - Direct access (flattened from entity)
* - `confidence`, `weight` - Entity confidence/importance (if set)
* - `entity` - Full Entity object (backward compatible)
* - `score` - Search relevance score (0-1)
*
* @example
* // Natural language queries (most common)
* const results = await brainy.find('users who work on AI projects')
@ -979,11 +1064,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* }
* })
*
* // Process results
* // NEW in v4.3.0: Access flattened fields directly
* for (const result of results) {
* console.log(`Found: ${result.entity.data} (score: ${result.score})`)
* console.log(`Score: ${result.score}`)
* console.log(`Type: ${result.type}`) // Flattened!
* console.log(`Metadata:`, result.metadata) // Flattened!
* console.log(`Confidence: ${result.confidence ?? 'N/A'}`) // Flattened!
* console.log(`Weight: ${result.weight ?? 'N/A'}`) // Flattened!
* }
*
* // Backward compatible: Nested access still works
* console.log(result.entity.data) // Also works
*
* @example
* // Metadata-only filtering (no vector search)
* const activeUsers = await brainy.find({
@ -1171,11 +1263,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
for (const id of pageIds) {
const entity = await this.get(id)
if (entity) {
results.push({
id,
score: 1.0, // All metadata-filtered results equally relevant
entity
})
results.push(this.createResult(id, 1.0, entity))
}
}
@ -1195,11 +1283,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
const noun = storageResults.items[i]
if (noun) {
const entity = await this.convertNounToEntity(noun)
results.push({
id: noun.id,
score: 1.0, // All results equally relevant for empty query
entity
})
results.push(this.createResult(noun.id, 1.0, entity))
}
}
@ -1305,11 +1389,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
for (const id of pageIds) {
const entity = await this.get(id)
if (entity) {
results.push({
id,
score: 1.0, // All metadata matches are equally relevant
entity: entity as Entity<T>
})
results.push(this.createResult(id, 1.0, entity))
}
}
@ -1350,7 +1430,15 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* Find similar entities using vector similarity
*
* @param params - Parameters specifying the target for similarity search
* @returns Promise that resolves to array of similar entities with similarity scores
* @param params.to - Entity ID, Entity object, or Vector to find similar to (required)
* @param params.limit - Maximum results (default: 10)
* @param params.threshold - Minimum similarity (0-1)
* @param params.type - Filter by NounType(s)
* @param params.where - Metadata filters
* @returns Promise that resolves to array of Result objects with similarity scores (same structure as find())
*
* **Returns (v4.3.0):**
* Same Result structure as find() with flattened fields for convenient access
*
* @example
* // Find entities similar to a specific entity by ID
@ -1359,9 +1447,12 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* limit: 10
* })
*
* // Process similarity results
* // NEW in v4.3.0: Access flattened fields
* for (const result of similarDocs) {
* console.log(`Similar entity: ${result.entity.data} (similarity: ${result.score})`)
* console.log(`Similarity: ${result.score}`)
* console.log(`Type: ${result.type}`) // Flattened!
* console.log(`Metadata:`, result.metadata) // Flattened!
* console.log(`Confidence: ${result.confidence ?? 'N/A'}`) // Flattened!
* }
*
* @example
@ -1987,6 +2078,32 @@ export class Brainy<T = any> implements BrainyInterface<T> {
/**
* Virtual File System API - Knowledge Operating System
*
* Returns a cached VFS instance. You must call vfs.init() before use:
*
* @example After import
* ```typescript
* await brain.import('./data.xlsx', { vfsPath: '/imports/data' })
*
* const vfs = brain.vfs()
* await vfs.init() // Required! (safe to call multiple times)
* const files = await vfs.readdir('/imports/data')
* ```
*
* @example Direct VFS usage
* ```typescript
* const vfs = brain.vfs()
* await vfs.init() // Always required before first use
* await vfs.writeFile('/docs/readme.md', 'Hello World')
* const content = await vfs.readFile('/docs/readme.md')
* ```
*
* **Note:** brain.import() automatically initializes the VFS, so after
* an import you can call vfs.init() again (it's idempotent) and immediately
* query the imported files.
*
* **Pattern:** The VFS instance is cached, so multiple calls to brain.vfs()
* return the same instance. This ensures import and user code share state.
*/
vfs(): VirtualFileSystem {
if (!this._vfs) {
@ -2601,7 +2718,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
const entity = await this.get(id)
if (entity) {
const score = Math.max(0, Math.min(1, 1 / (1 + distance)))
results.push({ id, score, entity })
results.push(this.createResult(id, score, entity))
}
}
@ -2625,11 +2742,11 @@ export class Brainy<T = any> implements BrainyInterface<T> {
const results: Result<T>[] = []
for (const [id, distance] of nearResults) {
const score = Math.max(0, Math.min(1, 1 / (1 + distance)))
if (score >= (params.near.threshold || 0.7)) {
const entity = await this.get(id)
if (entity) {
results.push({ id, score, entity })
results.push(this.createResult(id, score, entity))
}
}
}
@ -2668,11 +2785,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
for (const id of connectedIds) {
const entity = await this.get(id)
if (entity) {
results.push({
id,
score: 1.0,
entity
})
results.push(this.createResult(id, 1.0, entity))
}
}

View file

@ -66,23 +66,33 @@ export interface VFSStructureResult {
*/
export class VFSStructureGenerator {
private brain: Brainy
private vfs: VirtualFileSystem
private vfs!: VirtualFileSystem // Non-null assertion - will be set in init()
constructor(brain: Brainy) {
this.brain = brain
this.vfs = new VirtualFileSystem(brain)
// CRITICAL FIX: Use brain.vfs() instead of creating separate instance
// This ensures VFSStructureGenerator and user code share the same VFS instance
// Before: Created separate instance that wasn't accessible to users
// After: Uses brain's cached instance, making VFS queryable after import
}
/**
* Initialize the generator
*
* CRITICAL: Gets brain's VFS instance and initializes it if needed.
* This ensures that after import, brain.vfs() returns an initialized instance.
*/
async init(): Promise<void> {
// Always ensure VFS is initialized
// Get brain's cached VFS instance (creates if doesn't exist)
this.vfs = this.brain.vfs()
// Initialize if not already initialized
// VFS.init() is idempotent (safe to call multiple times)
try {
// Check if VFS is initialized by trying to access root
// Check if already initialized
await this.vfs.stat('/')
} catch (error) {
// VFS not initialized, initialize it
// Not initialized, initialize now
await this.vfs.init()
}
}

View file

@ -22,6 +22,8 @@ export interface Entity<T = any> {
createdAt: number
updatedAt?: number
createdBy?: string
confidence?: number // Type classification confidence (0-1)
weight?: number // Entity importance/salience (0-1)
}
/**
@ -59,11 +61,26 @@ export interface RelationEvidence {
/**
* Search result with similarity score
*
* Flattens commonly-used entity fields to top level for convenience,
* while preserving full entity in 'entity' field for backward compatibility.
*/
export interface Result<T = any> {
// Search metadata
id: string
score: number
// Convenience: Common entity fields flattened to top level
type?: NounType // Entity type (from entity.type)
metadata?: T // Entity metadata (from entity.metadata)
data?: any // Entity data (from entity.data)
confidence?: number // Type classification confidence (from entity.confidence)
weight?: number // Entity importance (from entity.weight)
// Full entity (preserved for backward compatibility)
entity: Entity<T>
// Score transparency
explanation?: ScoreExplanation
}
@ -90,6 +107,8 @@ export interface AddParams<T = any> {
id?: string // Optional custom ID
vector?: Vector // Pre-computed vector (skip embedding)
service?: string // Multi-tenancy support
confidence?: number // Type classification confidence (0-1)
weight?: number // Entity importance/salience (0-1)
}
/**
@ -102,6 +121,8 @@ export interface UpdateParams<T = any> {
metadata?: Partial<T> // Metadata to update
merge?: boolean // Merge or replace metadata (default: true)
vector?: Vector // New pre-computed vector
confidence?: number // Update type classification confidence
weight?: number // Update entity importance/salience
}
/**

View file

@ -1000,12 +1000,21 @@ export class VirtualFileSystem implements IVirtualFileSystem {
private async ensureInitialized(): Promise<void> {
if (!this.initialized) {
throw new Error(
'VFS not initialized. You must call await vfs.init() after getting the VFS instance.\n' +
'Example:\n' +
' const vfs = brain.vfs() // Note: vfs() is a method, not a property\n' +
' await vfs.init() // This creates the root directory\n' +
'See docs: https://github.com/Brainy-Technologies/brainy/blob/main/docs/vfs/QUICK_START.md'
throw new VFSError(
VFSErrorCode.EINVAL,
'VFS not initialized. Call await vfs.init() before using VFS operations.\n\n' +
'✅ After brain.import():\n' +
' await brain.import(file, { vfsPath: "/imports/data" })\n' +
' const vfs = brain.vfs()\n' +
' await vfs.init() // ← Required! Safe to call multiple times\n' +
' const files = await vfs.readdir("/imports/data")\n\n' +
'✅ Direct VFS usage:\n' +
' const vfs = brain.vfs()\n' +
' await vfs.init() // ← Always required before first use\n' +
' await vfs.writeFile("/docs/readme.md", "Hello")\n\n' +
'📖 Docs: https://github.com/soulcraftlabs/brainy/blob/main/docs/vfs/QUICK_START.md',
'<unknown>',
'VFS'
)
}
}