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
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue