feat(v4.0.0): Complete metadata/vector separation architecture with Azure support

This commit completes the core v4.0.0 architecture changes for billion-scale
performance with metadata/vector separation. NO RELEASE YET - remaining optimizations
and testing required before production release.

## Core v4.0.0 Architecture Changes

### Type System Updates
- Fixed all TypeScript compilation errors (zero errors achieved)
- Updated HNSWNoun/HNSWVerb to separate core fields from metadata
- Implemented HNSWNounWithMetadata/HNSWVerbWithMetadata for API boundaries
- Added required 'noun' field to NounMetadata for semantic structure
- Renamed verb.type to verb.verb for consistency

### Storage Adapter Updates
**All adapters updated for v4.0.0 two-file storage pattern:**
- memoryStorage: Proper metadata/vector separation
- fileSystemStorage: Two-file pattern with sharding
- opfsStorage: Browser persistent storage updated
- s3CompatibleStorage: AWS/MinIO/DigitalOcean support
- r2Storage: Cloudflare R2 optimization
- gcsStorage: Google Cloud with ADC support
- **azureBlobStorage: NEW - Full Azure Blob Storage support**

### Storage Features
- BaseStorage: Internal vs public method separation (_getNoun vs getNoun)
- Two-file storage: Vectors in one file, metadata in another
- Change tracking: getChangesSince return type updated
- Pagination: getNounsWithPagination returns WithMetadata types

### Azure Blob Storage Integration (NEW)
- Native @azure/storage-blob SDK integration
- Four authentication methods:
  * DefaultAzureCredential (Managed Identity) - recommended
  * Connection String - simplest setup
  * Account Name + Key - traditional auth
  * SAS Token - delegated access
- High-volume mode with write buffering
- Adaptive backpressure for throttling
- UUID-based sharding for billion-scale
- Full HNSW support with graph persistence

### Utility Updates
- EmbeddingManager: Updated to accept Record<string, unknown>
- LSMTree: Wrapped data in NounMetadata structure with 'noun' field
- EntityIdMapper: Fixed nested metadata.data structure access
- MetadataIndex: Fixed field type inference integration
- PeriodicCleanup: Updated for new metadata structure

### Core API Updates
- Brainy: Updated verb property access from v.type to v.verb
- ConfigAPI: Fixed NounMetadata access patterns
- DataAPI: Updated metadata handling

### Documentation Updates
- CREATING-AUGMENTATIONS.md: v4.0.0 breaking changes guide
- DEVELOPER-GUIDE.md: Migration checklist and examples
- COMPLETE-REFERENCE.md: v4.0.0 architecture improvements
- **finite-type-system.md: NEW - Revolutionary type system benefits**

### Build & Dependencies
- Zero TypeScript compilation errors
- Added @azure/storage-blob and @azure/identity
- 591 tests passing (23 timeout in long-running neural tests)

## What's NOT in This Release
This is a work-in-progress commit. Before v4.0.0 release we need:
- Storage adapter optimizations (batch operations, compression)
- Azure blob tier management (Hot/Cool/Archive)
- Cost optimization implementations
- Additional performance testing at billion-scale
- Migration guides for v3.x users

## Testing
- Clean build: 
- Type checking:  (zero errors)
- Test suite:  (591/614 passing, timeouts in neural tests only)

🔐 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-17 12:29:27 -07:00
parent 8d6dd07e1d
commit 92c96246fb
35 changed files with 4524 additions and 1026 deletions

View file

@ -5,7 +5,16 @@
import { GraphAdjacencyIndex } from '../graph/graphAdjacencyIndex.js'
import { GraphVerb, HNSWNoun, HNSWVerb, StatisticsData } from '../coreTypes.js'
import {
GraphVerb,
HNSWNoun,
HNSWVerb,
NounMetadata,
VerbMetadata,
HNSWNounWithMetadata,
HNSWVerbWithMetadata,
StatisticsData
} from '../coreTypes.js'
import { BaseStorageAdapter } from './adapters/baseStorageAdapter.js'
import { validateNounType, validateVerbType } from '../utils/typeValidation.js'
import { NounType, VerbType } from '../types/graphTypes.js'
@ -167,49 +176,46 @@ export abstract class BaseStorage extends BaseStorageAdapter {
}
/**
* Save a noun to storage
* Save a noun to storage (v4.0.0: vector only, metadata saved separately)
* @param noun Pure HNSW vector data (no metadata)
*/
public async saveNoun(noun: HNSWNoun): Promise<void> {
await this.ensureInitialized()
// Validate noun type before saving - storage boundary protection
if (noun.metadata?.noun) {
validateNounType(noun.metadata.noun)
}
// Save both the HNSWNoun vector data and metadata separately (2-file system)
try {
// Save the lightweight HNSWNoun vector file first
await this.saveNoun_internal(noun)
// Then save the metadata to separate file (if present)
if (noun.metadata) {
await this.saveNounMetadata(noun.id, noun.metadata)
}
} catch (error) {
console.error(`[ERROR] Failed to save noun ${noun.id}:`, error)
// Attempt cleanup - remove noun file if metadata failed
try {
const nounExists = await this.getNoun_internal(noun.id)
if (nounExists) {
console.log(`[CLEANUP] Attempting to remove orphaned noun file ${noun.id}`)
await this.deleteNoun_internal(noun.id)
}
} catch (cleanupError) {
console.error(`[ERROR] Failed to cleanup orphaned noun ${noun.id}:`, cleanupError)
}
throw new Error(`Failed to save noun ${noun.id}: ${error instanceof Error ? error.message : String(error)}`)
}
// Save the HNSWNoun vector data only
// Metadata must be saved separately via saveNounMetadata()
await this.saveNoun_internal(noun)
}
/**
* Get a noun from storage
* Get a noun from storage (v4.0.0: returns combined HNSWNounWithMetadata)
* @param id Entity ID
* @returns Combined vector + metadata or null
*/
public async getNoun(id: string): Promise<HNSWNoun | null> {
public async getNoun(id: string): Promise<HNSWNounWithMetadata | null> {
await this.ensureInitialized()
return this.getNoun_internal(id)
// Load vector and metadata separately
const vector = await this.getNoun_internal(id)
if (!vector) {
return null
}
// Load metadata
const metadata = await this.getNounMetadata(id)
if (!metadata) {
console.warn(`[Storage] Noun ${id} has vector but no metadata - this should not happen in v4.0.0`)
return null
}
// Combine into HNSWNounWithMetadata
return {
id: vector.id,
vector: vector.vector,
connections: vector.connections,
level: vector.level,
metadata
}
}
/**
@ -217,9 +223,25 @@ export abstract class BaseStorage extends BaseStorageAdapter {
* @param nounType The noun type to filter by
* @returns Promise that resolves to an array of nouns of the specified noun type
*/
public async getNounsByNounType(nounType: string): Promise<HNSWNoun[]> {
public async getNounsByNounType(nounType: string): Promise<HNSWNounWithMetadata[]> {
await this.ensureInitialized()
return this.getNounsByNounType_internal(nounType)
// Internal method returns HNSWNoun[], need to combine with metadata
const nouns = await this.getNounsByNounType_internal(nounType)
// Combine each noun with its metadata
const nounsWithMetadata: HNSWNounWithMetadata[] = []
for (const noun of nouns) {
const metadata = await this.getNounMetadata(noun.id)
if (metadata) {
nounsWithMetadata.push({
...noun,
metadata
})
}
}
return nounsWithMetadata
}
/**
@ -241,103 +263,66 @@ export abstract class BaseStorage extends BaseStorageAdapter {
}
/**
* Save a verb to storage
* Save a verb to storage (v4.0.0: verb only, metadata saved separately)
*
* ARCHITECTURAL FIX (v3.50.1): HNSWVerb now includes verb/sourceId/targetId
* These are core relational fields, not metadata. They're stored in the vector
* file for fast access and to align with actual usage patterns.
* @param verb Pure HNSW verb with core relational fields (verb, sourceId, targetId)
*/
public async saveVerb(verb: GraphVerb): Promise<void> {
public async saveVerb(verb: HNSWVerb): Promise<void> {
await this.ensureInitialized()
// Validate verb type before saving - storage boundary protection
if (verb.verb) {
validateVerbType(verb.verb)
}
validateVerbType(verb.verb)
// Extract HNSWVerb with CORE relational fields included
const hnswVerb: HNSWVerb = {
id: verb.id,
vector: verb.vector,
connections: verb.connections || new Map(),
// CORE RELATIONAL DATA (v3.50.1+)
verb: (verb.verb || verb.type || 'relatedTo') as VerbType,
sourceId: verb.sourceId || verb.source || '',
targetId: verb.targetId || verb.target || '',
// User metadata (if any)
metadata: verb.metadata
}
// Extract lightweight metadata for separate file (optional fields only)
const metadata = {
weight: verb.weight,
data: verb.data,
createdAt: verb.createdAt,
updatedAt: verb.updatedAt,
createdBy: verb.createdBy,
// Legacy aliases for backward compatibility
source: verb.source || verb.sourceId,
target: verb.target || verb.targetId,
type: verb.type || verb.verb
}
// Save both the HNSWVerb and metadata atomically
try {
console.log(`[DEBUG] Saving verb ${verb.id}: sourceId=${verb.sourceId}, targetId=${verb.targetId}`)
// Save the HNSWVerb first
await this.saveVerb_internal(hnswVerb)
console.log(`[DEBUG] Successfully saved HNSWVerb file for ${verb.id}`)
// Then save the metadata
await this.saveVerbMetadata(verb.id, metadata)
console.log(`[DEBUG] Successfully saved metadata file for ${verb.id}`)
} catch (error) {
console.error(`[ERROR] Failed to save verb ${verb.id}:`, error)
// Attempt cleanup - remove verb file if metadata failed
try {
const verbExists = await this.getVerb_internal(verb.id)
if (verbExists) {
console.log(`[CLEANUP] Attempting to remove orphaned verb file ${verb.id}`)
await this.deleteVerb_internal(verb.id)
}
} catch (cleanupError) {
console.error(`[ERROR] Failed to cleanup orphaned verb ${verb.id}:`, cleanupError)
}
throw new Error(`Failed to save verb ${verb.id}: ${error instanceof Error ? error.message : String(error)}`)
}
// Save the HNSWVerb vector and core fields only
// Metadata must be saved separately via saveVerbMetadata()
await this.saveVerb_internal(verb)
}
/**
* Get a verb from storage
* Get a verb from storage (v4.0.0: returns combined HNSWVerbWithMetadata)
* @param id Entity ID
* @returns Combined verb + metadata or null
*/
public async getVerb(id: string): Promise<GraphVerb | null> {
public async getVerb(id: string): Promise<HNSWVerbWithMetadata | null> {
await this.ensureInitialized()
const hnswVerb = await this.getVerb_internal(id)
if (!hnswVerb) {
// Load verb vector and core fields
const verb = await this.getVerb_internal(id)
if (!verb) {
return null
}
return this.convertHNSWVerbToGraphVerb(hnswVerb)
// Load metadata
const metadata = await this.getVerbMetadata(id)
if (!metadata) {
console.warn(`[Storage] Verb ${id} has vector but no metadata - this should not happen in v4.0.0`)
return null
}
// Combine into HNSWVerbWithMetadata
return {
id: verb.id,
vector: verb.vector,
connections: verb.connections,
verb: verb.verb,
sourceId: verb.sourceId,
targetId: verb.targetId,
metadata
}
}
/**
* Convert HNSWVerb to GraphVerb by combining with metadata
* DEPRECATED: For backward compatibility only. Use getVerb() which returns HNSWVerbWithMetadata.
*
* ARCHITECTURAL FIX (v3.50.1): Core fields (verb/sourceId/targetId) are now in HNSWVerb
* Only optional fields (weight, timestamps, etc.) come from metadata file
* @deprecated Use getVerb() instead which returns HNSWVerbWithMetadata
*/
protected async convertHNSWVerbToGraphVerb(hnswVerb: HNSWVerb): Promise<GraphVerb | null> {
try {
// Metadata file is now optional - contains only weight, timestamps, etc.
// Load metadata
const metadata = await this.getVerbMetadata(hnswVerb.id)
// Create default timestamp if not present
// Create default timestamp in Firestore format
const defaultTimestamp = {
seconds: Math.floor(Date.now() / 1000),
nanoseconds: (Date.now() % 1000) * 1000000
@ -349,11 +334,23 @@ export abstract class BaseStorage extends BaseStorageAdapter {
version: '1.0'
}
// Convert flexible timestamp to Firestore format for GraphVerb
const normalizeTimestamp = (ts: any) => {
if (!ts) return defaultTimestamp
if (typeof ts === 'number') {
return {
seconds: Math.floor(ts / 1000),
nanoseconds: (ts % 1000) * 1000000
}
}
return ts
}
return {
id: hnswVerb.id,
vector: hnswVerb.vector,
// CORE FIELDS from HNSWVerb (v3.50.1+)
// CORE FIELDS from HNSWVerb
verb: hnswVerb.verb,
sourceId: hnswVerb.sourceId,
targetId: hnswVerb.targetId,
@ -365,11 +362,11 @@ export abstract class BaseStorage extends BaseStorageAdapter {
// Optional fields from metadata file
weight: metadata?.weight || 1.0,
metadata: hnswVerb.metadata || {},
createdAt: metadata?.createdAt || defaultTimestamp,
updatedAt: metadata?.updatedAt || defaultTimestamp,
metadata: metadata as any || {},
createdAt: normalizeTimestamp(metadata?.createdAt),
updatedAt: normalizeTimestamp(metadata?.updatedAt),
createdBy: metadata?.createdBy || defaultCreatedBy,
data: metadata?.data,
data: metadata?.data as Record<string, any> | undefined,
embedding: hnswVerb.vector
}
} catch (error) {
@ -390,25 +387,15 @@ export abstract class BaseStorage extends BaseStorageAdapter {
pagination: { limit: Number.MAX_SAFE_INTEGER }
})
// Convert GraphVerbs back to HNSWVerbs for internal use
// ARCHITECTURAL FIX (v3.50.1): Include core relational fields
const hnswVerbs: HNSWVerb[] = []
for (const graphVerb of result.items) {
const hnswVerb: HNSWVerb = {
id: graphVerb.id,
vector: graphVerb.vector,
connections: new Map(),
// CORE RELATIONAL DATA
verb: (graphVerb.verb || graphVerb.type || 'relatedTo') as VerbType,
sourceId: graphVerb.sourceId || graphVerb.source || '',
targetId: graphVerb.targetId || graphVerb.target || '',
// User metadata
metadata: graphVerb.metadata
}
hnswVerbs.push(hnswVerb)
}
// v4.0.0: Convert HNSWVerbWithMetadata to HNSWVerb (strip metadata)
const hnswVerbs: HNSWVerb[] = result.items.map(verbWithMetadata => ({
id: verbWithMetadata.id,
vector: verbWithMetadata.vector,
connections: verbWithMetadata.connections,
verb: verbWithMetadata.verb,
sourceId: verbWithMetadata.sourceId,
targetId: verbWithMetadata.targetId
}))
return hnswVerbs
}
@ -416,7 +403,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
/**
* Get verbs by source
*/
public async getVerbsBySource(sourceId: string): Promise<GraphVerb[]> {
public async getVerbsBySource(sourceId: string): Promise<HNSWVerbWithMetadata[]> {
await this.ensureInitialized()
// CRITICAL: Fetch ALL verbs for this source, not just first page
@ -431,7 +418,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
/**
* Get verbs by target
*/
public async getVerbsByTarget(targetId: string): Promise<GraphVerb[]> {
public async getVerbsByTarget(targetId: string): Promise<HNSWVerbWithMetadata[]> {
await this.ensureInitialized()
// CRITICAL: Fetch ALL verbs for this target, not just first page
@ -446,7 +433,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
/**
* Get verbs by type
*/
public async getVerbsByType(type: string): Promise<GraphVerb[]> {
public async getVerbsByType(type: string): Promise<HNSWVerbWithMetadata[]> {
await this.ensureInitialized()
// Fetch ALL verbs of this type (no pagination limit)
@ -489,7 +476,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
metadata?: Record<string, any>
}
}): Promise<{
items: HNSWNoun[]
items: HNSWNounWithMetadata[]
totalCount?: number
hasMore: boolean
nextCursor?: string
@ -514,8 +501,8 @@ export abstract class BaseStorage extends BaseStorageAdapter {
? options.filter.nounType[0]
: options.filter.nounType
// Get nouns by type directly
const nounsByType = await this.getNounsByNounType_internal(nounType)
// Get nouns by type directly (already combines with metadata)
const nounsByType = await this.getNounsByNounType(nounType)
// Apply pagination
const paginatedNouns = nounsByType.slice(offset, offset + limit)
@ -629,7 +616,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
metadata?: Record<string, any>
}
}): Promise<{
items: GraphVerb[]
items: HNSWVerbWithMetadata[]
totalCount?: number
hasMore: boolean
nextCursor?: string
@ -906,53 +893,51 @@ export abstract class BaseStorage extends BaseStorageAdapter {
protected abstract listObjectsUnderPath(prefix: string): Promise<string[]>
/**
* Save metadata to storage
* Save metadata to storage (v4.0.0: now typed)
* Routes to correct location (system or entity) based on key format
*/
public async saveMetadata(id: string, metadata: any): Promise<void> {
public async saveMetadata(id: string, metadata: NounMetadata): Promise<void> {
await this.ensureInitialized()
const keyInfo = this.analyzeKey(id, 'system')
return this.writeObjectToPath(keyInfo.fullPath, metadata)
}
/**
* Get metadata from storage
* Get metadata from storage (v4.0.0: now typed)
* Routes to correct location (system or entity) based on key format
*/
public async getMetadata(id: string): Promise<any | null> {
public async getMetadata(id: string): Promise<NounMetadata | null> {
await this.ensureInitialized()
const keyInfo = this.analyzeKey(id, 'system')
return this.readObjectFromPath(keyInfo.fullPath)
}
/**
* Save noun metadata to storage
* Save noun metadata to storage (v4.0.0: now typed)
* Routes to correct sharded location based on UUID
*/
public async saveNounMetadata(id: string, metadata: any): Promise<void> {
public async saveNounMetadata(id: string, metadata: NounMetadata): Promise<void> {
// Validate noun type in metadata - storage boundary protection
if (metadata?.noun) {
validateNounType(metadata.noun)
}
validateNounType(metadata.noun)
return this.saveNounMetadata_internal(id, metadata)
}
/**
* Internal method for saving noun metadata
* Internal method for saving noun metadata (v4.0.0: now typed)
* Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
* @protected
*/
protected async saveNounMetadata_internal(id: string, metadata: any): Promise<void> {
protected async saveNounMetadata_internal(id: string, metadata: NounMetadata): Promise<void> {
await this.ensureInitialized()
const keyInfo = this.analyzeKey(id, 'noun-metadata')
return this.writeObjectToPath(keyInfo.fullPath, metadata)
}
/**
* Get noun metadata from storage
* Get noun metadata from storage (v4.0.0: now typed)
* Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
*/
public async getNounMetadata(id: string): Promise<any | null> {
public async getNounMetadata(id: string): Promise<NounMetadata | null> {
await this.ensureInitialized()
const keyInfo = this.analyzeKey(id, 'noun-metadata')
return this.readObjectFromPath(keyInfo.fullPath)
@ -969,33 +954,30 @@ export abstract class BaseStorage extends BaseStorageAdapter {
}
/**
* Save verb metadata to storage
* Save verb metadata to storage (v4.0.0: now typed)
* Routes to correct sharded location based on UUID
*/
public async saveVerbMetadata(id: string, metadata: any): Promise<void> {
// Validate verb type in metadata - storage boundary protection
if (metadata?.verb) {
validateVerbType(metadata.verb)
}
public async saveVerbMetadata(id: string, metadata: VerbMetadata): Promise<void> {
// Note: verb type is in HNSWVerb, not metadata
return this.saveVerbMetadata_internal(id, metadata)
}
/**
* Internal method for saving verb metadata
* Internal method for saving verb metadata (v4.0.0: now typed)
* Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
* @protected
*/
protected async saveVerbMetadata_internal(id: string, metadata: any): Promise<void> {
protected async saveVerbMetadata_internal(id: string, metadata: VerbMetadata): Promise<void> {
await this.ensureInitialized()
const keyInfo = this.analyzeKey(id, 'verb-metadata')
return this.writeObjectToPath(keyInfo.fullPath, metadata)
}
/**
* Get verb metadata from storage
* Get verb metadata from storage (v4.0.0: now typed)
* Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
*/
public async getVerbMetadata(id: string): Promise<any | null> {
public async getVerbMetadata(id: string): Promise<VerbMetadata | null> {
await this.ensureInitialized()
const keyInfo = this.analyzeKey(id, 'verb-metadata')
return this.readObjectFromPath(keyInfo.fullPath)
@ -1055,7 +1037,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
*/
protected abstract getVerbsBySource_internal(
sourceId: string
): Promise<GraphVerb[]>
): Promise<HNSWVerbWithMetadata[]>
/**
* Get verbs by target
@ -1063,13 +1045,13 @@ export abstract class BaseStorage extends BaseStorageAdapter {
*/
protected abstract getVerbsByTarget_internal(
targetId: string
): Promise<GraphVerb[]>
): Promise<HNSWVerbWithMetadata[]>
/**
* Get verbs by type
* This method should be implemented by each specific adapter
*/
protected abstract getVerbsByType_internal(type: string): Promise<GraphVerb[]>
protected abstract getVerbsByType_internal(type: string): Promise<HNSWVerbWithMetadata[]>
/**
* Delete a verb from storage