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:
David Snelling 2025-09-01 09:37:36 -07:00
parent d7d2d749b6
commit 6c62bc4e9d
40 changed files with 1704 additions and 497 deletions

326
src/utils/brainyTypes.ts Normal file
View 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
View 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
}
}