feat: add comprehensive zero-config validation system

- Implement self-configuring validation that adapts to system resources
- Add validation for all CRUD operations (add, update, delete, find, relate)
- Auto-configure limits based on available memory (1GB = 10K limit, 8GB = 80K)
- Monitor and auto-tune performance based on query response times
- Fix multiple type filtering with proper anyOf structure
- Enhance type safety by requiring NounType/VerbType enums
- Fix tests to validate correct behavior (no fake implementations)
- Add comprehensive VALIDATION.md documentation
- Update API_REFERENCE.md with validation rules and examples
- Clarify metadata update behavior (null keeps existing, {} clears)

BREAKING CHANGE: getFieldsForType() now requires NounType enum instead of string

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-09-12 14:37:39 -07:00
parent 887a725677
commit 008abb1cab
12 changed files with 979 additions and 76 deletions

View file

@ -14,7 +14,6 @@ import {
defaultEmbeddingFunction,
cosineDistance
} from './utils/index.js'
import { validateNounType, validateVerbType } from './utils/typeValidation.js'
import { matchesMetadataFilter } from './utils/metadataFilter.js'
import { AugmentationRegistry, AugmentationContext } from './augmentations/brainyAugmentation.js'
import { createDefaultAugmentations } from './augmentations/defaultAugmentations.js'
@ -165,11 +164,9 @@ export class Brainy<T = any> {
async add(params: AddParams<T>): Promise<string> {
await this.ensureInitialized()
// Validate parameters
if (!params.data && !params.vector) {
throw new Error('Either data or vector is required')
}
validateNounType(params.type)
// Zero-config validation
const { validateAddParams } = await import('./utils/paramValidation.js')
validateAddParams(params)
// Generate ID if not provided
const id = params.id || uuidv4()
@ -266,9 +263,9 @@ export class Brainy<T = any> {
async update(params: UpdateParams<T>): Promise<void> {
await this.ensureInitialized()
if (!params.id) {
throw new Error('ID is required for update')
}
// Zero-config validation
const { validateUpdateParams } = await import('./utils/paramValidation.js')
validateUpdateParams(params)
return this.augmentationRegistry.execute('update', params, async () => {
// Get existing entity
@ -363,11 +360,9 @@ export class Brainy<T = any> {
async relate(params: RelateParams<T>): Promise<string> {
await this.ensureInitialized()
// Validate parameters
if (!params.from || !params.to) {
throw new Error('Both from and to are required')
}
validateVerbType(params.type)
// Zero-config validation
const { validateRelateParams } = await import('./utils/paramValidation.js')
validateRelateParams(params)
// Verify entities exist
const fromEntity = await this.get(params.from)
@ -495,7 +490,12 @@ export class Brainy<T = any> {
const params: FindParams<T> =
typeof query === 'string' ? await this.parseNaturalQuery(query) : query
return this.augmentationRegistry.execute('find', params, async () => {
// Zero-config validation - only enforces universal truths
const { validateFindParams, recordQueryPerformance } = await import('./utils/paramValidation.js')
validateFindParams(params)
const startTime = Date.now()
const result = await this.augmentationRegistry.execute('find', params, async () => {
let results: Result<T>[] = []
// Handle empty query - return paginated results from storage
@ -561,13 +561,26 @@ export class Brainy<T = any> {
// Apply O(log n) metadata filtering using core MetadataIndexManager
if (params.where || params.type || params.service) {
// Build filter object for metadata index
const filter: any = {}
let filter: any = {}
// Base filter from where and service
if (params.where) Object.assign(filter, params.where)
if (params.service) filter.service = params.service
if (params.type) {
const types = Array.isArray(params.type) ? params.type : [params.type]
filter.noun = types.length === 1 ? types[0] : { anyOf: types }
if (types.length === 1) {
filter.noun = types[0]
} else {
// For multiple types, create separate filter for each type with all conditions
filter = {
anyOf: types.map(type => ({
noun: type,
...filter
}))
}
}
}
if (params.service) filter.service = params.service
const filteredIds = await this.metadataIndex.getIdsForFilter(filter)
@ -608,6 +621,12 @@ export class Brainy<T = any> {
return results.slice(offset, offset + limit)
})
// Record performance for auto-tuning
const duration = Date.now() - startTime
recordQueryPerformance(duration, result.length)
return result
}
/**
@ -953,7 +972,7 @@ export class Brainy<T = any> {
* Get fields that commonly appear with a specific entity type
* Essential for type-aware NLP parsing
*/
async getFieldsForType(nounType: string): Promise<Array<{
async getFieldsForType(nounType: NounType): Promise<Array<{
field: string
affinity: number
occurrences: number

View file

@ -385,7 +385,7 @@ export class NaturalLanguageProcessor {
reason?: string
}> {
// Get fields that actually appear with this type
const typeFields = await this.brain.getFieldsForType(nounType)
const typeFields = await this.brain.getFieldsForType(nounType as NounType)
// Check if this field appears with this type
const fieldInfo = typeFields.find(tf => tf.field === field)
@ -554,7 +554,7 @@ export class NaturalLanguageProcessor {
// Step 4: Get type-specific fields if we detected a type
let typeSpecificFields: Array<{field: string; affinity: number}> = []
if (detectedNounType && typeConfidence > 0.75) {
const fieldsForType = await this.brain.getFieldsForType(detectedNounType)
const fieldsForType = await this.brain.getFieldsForType(detectedNounType as NounType)
typeSpecificFields = fieldsForType.map(f => ({field: f.field, affinity: f.affinity}))
}

View file

@ -8,6 +8,7 @@ import { StorageAdapter } from '../coreTypes.js'
import { MetadataIndexCache, MetadataIndexCacheConfig } from './metadataIndexCache.js'
import { prodLog } from './logger.js'
import { getGlobalCache, UnifiedCache } from './unifiedCache.js'
import { NounType } from '../types/graphTypes.js'
export interface MetadataIndexEntry {
field: string
@ -1959,7 +1960,7 @@ export class MetadataIndexManager {
* Get fields that commonly appear with a specific entity type
* Returns fields with their affinity scores (0-1)
*/
async getFieldsForType(nounType: string): Promise<Array<{
async getFieldsForType(nounType: NounType): Promise<Array<{
field: string
affinity: number
occurrences: number

View file

@ -0,0 +1,237 @@
/**
* Zero-Config Parameter Validation
*
* Self-configuring validation that adapts to system capabilities
* Only enforces universal truths, learns everything else
*/
import { FindParams, AddParams, UpdateParams, RelateParams } from '../types/brainy.types.js'
import { NounType, VerbType } from '../types/graphTypes.js'
import * as os from 'os'
/**
* Auto-configured limits based on system resources
* These adapt to available memory and observed performance
*/
class ValidationConfig {
private static instance: ValidationConfig
// Dynamic limits based on system
public maxLimit: number
public maxQueryLength: number
public maxVectorDimensions: number
// Performance observations
private avgQueryTime: number = 0
private queryCount: number = 0
private constructor() {
// Auto-configure based on system resources
const totalMemory = os.totalmem()
const availableMemory = os.freemem()
// Scale limits based on available memory
// 1GB = 10K limit, 8GB = 80K limit, etc.
this.maxLimit = Math.min(
100000, // Absolute max for safety
Math.floor(availableMemory / (1024 * 1024 * 100)) * 1000
)
// Query length scales with memory too
this.maxQueryLength = Math.min(
50000,
Math.floor(availableMemory / (1024 * 1024 * 10)) * 1000
)
// Vector dimensions (standard for all-MiniLM-L6-v2)
this.maxVectorDimensions = 384
}
static getInstance(): ValidationConfig {
if (!ValidationConfig.instance) {
ValidationConfig.instance = new ValidationConfig()
}
return ValidationConfig.instance
}
/**
* Learn from actual usage to adjust limits
*/
recordQuery(duration: number, resultCount: number) {
this.queryCount++
this.avgQueryTime = (this.avgQueryTime * (this.queryCount - 1) + duration) / this.queryCount
// If queries are consistently fast with large results, increase limits
if (this.avgQueryTime < 100 && resultCount > this.maxLimit * 0.8) {
this.maxLimit = Math.min(this.maxLimit * 1.5, 100000)
}
// If queries are slow, reduce limits
if (this.avgQueryTime > 1000) {
this.maxLimit = Math.max(this.maxLimit * 0.8, 1000)
}
}
}
/**
* Universal validations - things that are always invalid
* These are mathematical/logical truths, not configuration
*/
export function validateFindParams(params: FindParams): void {
const config = ValidationConfig.getInstance()
// Universal truth: negative pagination never makes sense
if (params.limit !== undefined) {
if (params.limit < 0) {
throw new Error('limit must be non-negative')
}
if (params.limit > config.maxLimit) {
throw new Error(`limit exceeds auto-configured maximum of ${config.maxLimit} (based on available memory)`)
}
}
if (params.offset !== undefined && params.offset < 0) {
throw new Error('offset must be non-negative')
}
// Universal truth: probability/similarity must be 0-1
if (params.near?.threshold !== undefined) {
const t = params.near.threshold
if (t < 0 || t > 1) {
throw new Error('threshold must be between 0 and 1')
}
}
// Universal truth: can't specify both query and vector (they're alternatives)
if (params.query !== undefined && params.vector !== undefined) {
throw new Error('cannot specify both query and vector - they are mutually exclusive')
}
// Universal truth: can't use both cursor and offset pagination
if (params.cursor !== undefined && params.offset !== undefined) {
throw new Error('cannot use both cursor and offset pagination simultaneously')
}
// Auto-limit query length based on memory
if (params.query && params.query.length > config.maxQueryLength) {
throw new Error(`query exceeds auto-configured maximum length of ${config.maxQueryLength} characters`)
}
// Validate vector dimensions if provided
if (params.vector && params.vector.length !== config.maxVectorDimensions) {
throw new Error(`vector must have exactly ${config.maxVectorDimensions} dimensions`)
}
// Validate enum types if specified
if (params.type) {
const types = Array.isArray(params.type) ? params.type : [params.type]
for (const type of types) {
if (!Object.values(NounType).includes(type)) {
throw new Error(`invalid NounType: ${type}`)
}
}
}
}
/**
* Validate add parameters
*/
export function validateAddParams(params: AddParams): void {
// Universal truth: must have data or vector
if (!params.data && !params.vector) {
throw new Error('must provide either data or vector')
}
// Validate noun type
if (!Object.values(NounType).includes(params.type)) {
throw new Error(`invalid NounType: ${params.type}`)
}
// Validate vector dimensions if provided
if (params.vector) {
const config = ValidationConfig.getInstance()
if (params.vector.length !== config.maxVectorDimensions) {
throw new Error(`vector must have exactly ${config.maxVectorDimensions} dimensions`)
}
}
}
/**
* Validate update parameters
*/
export function validateUpdateParams(params: UpdateParams): void {
// Universal truth: must have an ID
if (!params.id) {
throw new Error('id is required for update')
}
// Universal truth: must update something
if (!params.data && !params.metadata && !params.type && !params.vector) {
throw new Error('must specify at least one field to update')
}
// Validate type if changing
if (params.type && !Object.values(NounType).includes(params.type)) {
throw new Error(`invalid NounType: ${params.type}`)
}
// Validate vector dimensions if provided
if (params.vector) {
const config = ValidationConfig.getInstance()
if (params.vector.length !== config.maxVectorDimensions) {
throw new Error(`vector must have exactly ${config.maxVectorDimensions} dimensions`)
}
}
}
/**
* Validate relate parameters
*/
export function validateRelateParams(params: RelateParams): void {
// Universal truths
if (!params.from) {
throw new Error('from entity ID is required')
}
if (!params.to) {
throw new Error('to entity ID is required')
}
if (params.from === params.to) {
throw new Error('cannot create self-referential relationship')
}
// Validate verb type
if (!Object.values(VerbType).includes(params.type)) {
throw new Error(`invalid VerbType: ${params.type}`)
}
// Universal truth: weight must be 0-1
if (params.weight !== undefined) {
if (params.weight < 0 || params.weight > 1) {
throw new Error('weight must be between 0 and 1')
}
}
}
/**
* Get current validation configuration
* Useful for debugging and monitoring
*/
export function getValidationConfig() {
const config = ValidationConfig.getInstance()
return {
maxLimit: config.maxLimit,
maxQueryLength: config.maxQueryLength,
maxVectorDimensions: config.maxVectorDimensions,
systemMemory: os.totalmem(),
availableMemory: os.freemem()
}
}
/**
* Record query performance for auto-tuning
*/
export function recordQueryPerformance(duration: number, resultCount: number) {
ValidationConfig.getInstance().recordQuery(duration, resultCount)
}