This repository has been archived on 2026-09-03. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
open-brainy/src/neural/signals/ExactMatchSignal.ts
David Snelling c44678390e feat(8.0): brain.fillSubtypes migration helper + pre-RC1 gap closure
- brain.fillSubtypes(rules): idempotent subtype back-fill for pre-8.0 data.
  One rule per NounType/VerbType (literal default or per-entry function);
  fills only entries still missing a subtype through the public update()/
  updateRelation() paths; returns { scanned, filled, skipped, errors, byType }.
  Full unit suite in tests/unit/brainy/fill-subtypes.test.ts.
- Fix getNouns/getVerbs pagination hasMore (peek one past the window) —
  was permanently false, silently truncating every multi-page walk.
- find({ near }) without near.id now throws a teaching error instead of an
  opaque storage sharding failure; CLI --threshold without --near applies a
  plain score floor.
- CLI init/close audit: every one-shot command init()s, close()s, and exits
  explicitly; delete the unmaintained interactive REPL; replace the cloud-era
  storage subcommands with status/batch-delete; new types/validate commands.
- requireSubtype JSDoc now documents the 8.0 default-on contract; audit()
  recommendation points at fillSubtypes.
- Docs: data-storage-architecture rewritten to the real 8.0 on-disk layout;
  README storage section reflects filesystem+memory and snapshots; eli5 and
  SEMANTIC_VFS /as-of/ semantics corrected; internal tracker IDs and
  .strategy references scrubbed from published files.
2026-06-11 10:53:55 -07:00

712 lines
19 KiB
TypeScript

/**
* ExactMatchSignal - O(1) exact match entity type classification
*
* HIGHEST WEIGHT: 40% (most reliable signal)
*
* Uses:
* 1. O(1) term index lookup (exact string match)
* 2. O(1) metadata hints (column names, file structure)
* 3. Format-specific intelligence (Excel, CSV, PDF, YAML, DOCX)
*
* Finds explicit relationships via exact matching — added after a consumer
* report of extraction missing explicitly-named relationships.
*/
import type { Brainy } from '../../brainy.js'
import { NounType } from '../../types/graphTypes.js'
import type { Vector } from '../../coreTypes.js'
/**
* Signal result with classification details
*/
export interface TypeSignal {
source: 'exact-term' | 'exact-metadata' | 'exact-format'
type: NounType
confidence: number
evidence: string
metadata?: {
matchedTerm?: string
columnHint?: string
formatHint?: string
}
}
/**
* Options for exact match signal
*/
export interface ExactMatchSignalOptions {
minConfidence?: number // Minimum confidence threshold (default: 0.85)
cacheSize?: number // LRU cache size (default: 5000)
// Format-specific detection
enableFormatHints?: boolean // Use format-specific intelligence (default: true)
// Metadata column detection patterns
columnPatterns?: {
term?: string[] // ["Term", "Name", "Title", "Entity"]
type?: string[] // ["Type", "Category", "Kind"]
definition?: string[] // ["Definition", "Description", "Text"]
related?: string[] // ["Related", "See Also", "References"]
}
}
/**
* Term index entry with type information
*/
interface TermEntry {
term: string // Original term text
type: NounType // Classified type
confidence: number // Classification confidence
source: string // Where it came from
}
/**
* Format-specific hint from file structure
*/
interface FormatHint {
type: NounType
confidence: number
evidence: string
}
/**
* ExactMatchSignal - Instant O(1) type classification via exact matching
*
* Production features:
* - O(1) hash table lookups (fastest possible)
* - Format-specific intelligence (Excel columns, CSV headers, etc.)
* - Metadata hints (column names reveal entity types)
* - LRU cache for hot paths
* - Highest confidence (0.95-0.99) - most reliable signal
*/
export class ExactMatchSignal {
private brain: Brainy
private options: Required<ExactMatchSignalOptions>
// O(1) term lookup index (key: normalized term → value: type info)
private termIndex: Map<string, TermEntry> = new Map()
// LRU cache for hot lookups
private cache: Map<string, TypeSignal | null> = new Map()
private cacheOrder: string[] = []
// Statistics
private stats = {
calls: 0,
cacheHits: 0,
termMatches: 0,
metadataMatches: 0,
formatMatches: 0
}
constructor(brain: Brainy, options?: ExactMatchSignalOptions) {
this.brain = brain
this.options = {
minConfidence: options?.minConfidence ?? 0.85,
cacheSize: options?.cacheSize ?? 5000,
enableFormatHints: options?.enableFormatHints ?? true,
columnPatterns: {
term: options?.columnPatterns?.term ?? ['term', 'name', 'title', 'entity', 'concept'],
type: options?.columnPatterns?.type ?? ['type', 'category', 'kind', 'class'],
definition: options?.columnPatterns?.definition ?? ['definition', 'description', 'text', 'content'],
related: options?.columnPatterns?.related ?? ['related', 'see also', 'references', 'links']
}
}
}
/**
* Build term index from import data (call once per import)
*
* This is O(n) upfront cost, then O(1) lookups forever
*
* @param terms Array of terms with their types
*/
buildIndex(terms: Array<{ text: string, type: NounType, confidence?: number }>): void {
this.termIndex.clear()
for (const term of terms) {
const normalized = this.normalize(term.text)
// Index full term
this.termIndex.set(normalized, {
term: term.text,
type: term.type,
confidence: term.confidence ?? 1.0,
source: 'index'
})
// Also index individual tokens for multi-word terms
const tokens = this.tokenize(normalized)
for (const token of tokens) {
if (token.length >= 3 && !this.termIndex.has(token)) {
this.termIndex.set(token, {
term: term.text,
type: term.type,
confidence: (term.confidence ?? 1.0) * 0.8, // Slight discount for partial match
source: 'token'
})
}
}
}
}
/**
* Classify entity type using exact matching
*
* Main entry point - checks term index, metadata, and format hints
*
* @param candidate Entity text to classify
* @param context Optional context for better matching
* @returns TypeSignal with classification result or null
*/
async classify(
candidate: string,
context?: {
definition?: string
metadata?: Record<string, any>
columnName?: string
fileFormat?: 'excel' | 'csv' | 'pdf' | 'json' | 'markdown' | 'yaml' | 'docx'
rowData?: Record<string, any>
}
): Promise<TypeSignal | null> {
this.stats.calls++
// Check cache first (O(1))
const cacheKey = this.getCacheKey(candidate, context)
const cached = this.getFromCache(cacheKey)
if (cached !== undefined) {
this.stats.cacheHits++
return cached
}
// Try exact term match (O(1))
const termMatch = this.matchTerm(candidate)
if (termMatch && termMatch.confidence >= this.options.minConfidence) {
this.stats.termMatches++
this.addToCache(cacheKey, termMatch)
return termMatch
}
// Try metadata hints (O(1))
if (context?.metadata || context?.columnName) {
const metadataMatch = this.matchMetadata(candidate, context)
if (metadataMatch && metadataMatch.confidence >= this.options.minConfidence) {
this.stats.metadataMatches++
this.addToCache(cacheKey, metadataMatch)
return metadataMatch
}
}
// Try format-specific hints
if (this.options.enableFormatHints && context?.fileFormat) {
const formatMatch = this.matchFormat(candidate, context)
if (formatMatch && formatMatch.confidence >= this.options.minConfidence) {
this.stats.formatMatches++
this.addToCache(cacheKey, formatMatch)
return formatMatch
}
}
// No match found - cache null to avoid recomputation
this.addToCache(cacheKey, null)
return null
}
/**
* Match against term index (O(1))
*
* Highest confidence - exact string match
*/
private matchTerm(candidate: string): TypeSignal | null {
const normalized = this.normalize(candidate)
const entry = this.termIndex.get(normalized)
if (!entry) return null
return {
source: 'exact-term',
type: entry.type,
confidence: entry.confidence * 0.99, // 0.99 for exact term match
evidence: `Exact match in term index: "${entry.term}"`,
metadata: {
matchedTerm: entry.term
}
}
}
/**
* Match using metadata hints (column names, file structure)
*
* High confidence - structural clues reveal entity types
*/
private matchMetadata(
candidate: string,
context: {
metadata?: Record<string, any>
columnName?: string
rowData?: Record<string, any>
}
): TypeSignal | null {
// Check column name patterns
if (context.columnName) {
const hint = this.detectColumnType(context.columnName, context.rowData)
if (hint) {
return {
source: 'exact-metadata',
type: hint.type,
confidence: hint.confidence * 0.95, // 0.95 for metadata hints
evidence: hint.evidence,
metadata: {
columnHint: context.columnName
}
}
}
}
// Check explicit type metadata
if (context.metadata?.type) {
const hint = this.inferTypeFromMetadata(context.metadata.type)
if (hint) {
return {
source: 'exact-metadata',
type: hint.type,
confidence: hint.confidence * 0.98, // 0.98 for explicit type
evidence: hint.evidence,
metadata: {
columnHint: 'type'
}
}
}
}
return null
}
/**
* Match using format-specific intelligence
*
* Excel, CSV, PDF, YAML, DOCX each have unique structural patterns
*/
private matchFormat(
candidate: string,
context: {
fileFormat?: string
rowData?: Record<string, any>
definition?: string
metadata?: Record<string, any>
}
): TypeSignal | null {
if (!context.fileFormat) return null
switch (context.fileFormat) {
case 'excel':
return this.detectExcelPatterns(candidate, context)
case 'csv':
return this.detectCSVPatterns(candidate, context)
case 'pdf':
return this.detectPDFPatterns(candidate, context)
case 'yaml':
return this.detectYAMLPatterns(candidate, context)
case 'docx':
return this.detectDOCXPatterns(candidate, context)
default:
return null
}
}
/**
* Detect Excel-specific patterns
*
* - Cell formats (dates, currencies)
* - Named ranges
* - Column headers reveal entity types
* - Sheet names as categories
*/
private detectExcelPatterns(
candidate: string,
context: { rowData?: Record<string, any>, metadata?: Record<string, any> }
): TypeSignal | null {
// Sheet name hints
if (context.metadata?.sheetName) {
const sheetHint = this.inferTypeFromSheetName(context.metadata.sheetName)
if (sheetHint) {
return {
source: 'exact-format',
type: sheetHint.type,
confidence: sheetHint.confidence * 0.90,
evidence: `Excel sheet name: "${context.metadata.sheetName}"`,
metadata: { formatHint: 'excel-sheet' }
}
}
}
// Column position hints (first column often = entity name)
if (context.metadata?.columnIndex === 0) {
// First column is often the primary entity
// But don't return a type without more evidence
}
return null
}
/**
* Detect CSV-specific patterns
*
* - Relationship columns (parent_id, created_by)
* - Nested delimiters (semicolons, pipes)
* - URL columns indicate external references
*/
private detectCSVPatterns(
candidate: string,
context: { rowData?: Record<string, any> }
): TypeSignal | null {
if (!context.rowData) return null
// Check for relationship columns
const keys = Object.keys(context.rowData)
// parent_id → indicates hierarchical structure
if (keys.some(k => k.toLowerCase().includes('parent'))) {
// This entity is part of a hierarchy
}
// URL column → external reference
const urlPattern = /^https?:\/\//
if (typeof candidate === 'string' && urlPattern.test(candidate)) {
// Don't classify URLs as entities - they're references
return null
}
return null
}
/**
* Detect PDF-specific patterns
*
* - Table of contents entries
* - Section headings
* - Citation references
* - Figure captions
*/
private detectPDFPatterns(
candidate: string,
context: { metadata?: Record<string, any> }
): TypeSignal | null {
// TOC entry → likely a concept or topic
if (context.metadata?.isTOCEntry) {
return {
source: 'exact-format',
type: NounType.Concept,
confidence: 0.88,
evidence: 'PDF table of contents entry',
metadata: { formatHint: 'pdf-toc' }
}
}
return null
}
/**
* Detect YAML-specific patterns
*
* - Key names reveal entity types
* - Nested structure indicates relationships
* - Lists indicate collections
*/
private detectYAMLPatterns(
candidate: string,
context: { metadata?: Record<string, any> }
): TypeSignal | null {
if (!context.metadata?.yamlKey) return null
const key = context.metadata.yamlKey.toLowerCase()
// Common YAML patterns
if (key.includes('user') || key.includes('author')) {
return {
source: 'exact-format',
type: NounType.Person,
confidence: 0.90,
evidence: `YAML key indicates person: "${context.metadata.yamlKey}"`,
metadata: { formatHint: 'yaml-key' }
}
}
if (key.includes('organization') || key.includes('company')) {
return {
source: 'exact-format',
type: NounType.Organization,
confidence: 0.92,
evidence: `YAML key indicates organization: "${context.metadata.yamlKey}"`,
metadata: { formatHint: 'yaml-key' }
}
}
return null
}
/**
* Detect DOCX-specific patterns
*
* - Heading levels indicate hierarchy
* - List items indicate collections
* - Comments indicate relationships
* - Track changes reveal authorship
*/
private detectDOCXPatterns(
candidate: string,
context: { metadata?: Record<string, any> }
): TypeSignal | null {
// Heading level → concept hierarchy
if (context.metadata?.headingLevel) {
return {
source: 'exact-format',
type: NounType.Concept,
confidence: 0.87,
evidence: `DOCX heading (level ${context.metadata.headingLevel})`,
metadata: { formatHint: 'docx-heading' }
}
}
return null
}
/**
* Detect entity type from column name patterns
*/
private detectColumnType(
columnName: string,
rowData?: Record<string, any>
): FormatHint | null {
const lower = columnName.toLowerCase()
// Location indicators
if (lower.includes('location') || lower.includes('place') ||
lower.includes('city') || lower.includes('country')) {
return {
type: NounType.Location,
confidence: 0.92,
evidence: `Column name indicates location: "${columnName}"`
}
}
// Person indicators
if (lower.includes('person') || lower.includes('author') ||
lower.includes('user') || lower.includes('name') &&
(lower.includes('first') || lower.includes('last'))) {
return {
type: NounType.Person,
confidence: 0.90,
evidence: `Column name indicates person: "${columnName}"`
}
}
// Organization indicators
if (lower.includes('organization') || lower.includes('company') ||
lower.includes('institution') || lower.includes('org')) {
return {
type: NounType.Organization,
confidence: 0.91,
evidence: `Column name indicates organization: "${columnName}"`
}
}
return null
}
/**
* Infer type from explicit type metadata
*/
private inferTypeFromMetadata(typeValue: any): FormatHint | null {
if (typeof typeValue !== 'string') return null
const lower = typeValue.toLowerCase()
// Direct mapping
const typeMap: Record<string, NounType> = {
'person': NounType.Person,
'people': NounType.Person,
'location': NounType.Location,
'place': NounType.Location,
'organization': NounType.Organization,
'company': NounType.Organization,
'concept': NounType.Concept,
'idea': NounType.Concept,
'event': NounType.Event,
'document': NounType.Document,
'file': NounType.File,
'product': NounType.Product,
'service': NounType.Service
}
const type = typeMap[lower]
if (type) {
return {
type,
confidence: 0.98,
evidence: `Explicit type metadata: "${typeValue}"`
}
}
return null
}
/**
* Infer type from Excel sheet name
*/
private inferTypeFromSheetName(sheetName: string): FormatHint | null {
const lower = sheetName.toLowerCase()
if (lower.includes('character') || lower.includes('people') || lower.includes('person')) {
return {
type: NounType.Person,
confidence: 0.88,
evidence: `Sheet name suggests people: "${sheetName}"`
}
}
if (lower.includes('location') || lower.includes('place') || lower.includes('map')) {
return {
type: NounType.Location,
confidence: 0.87,
evidence: `Sheet name suggests locations: "${sheetName}"`
}
}
if (lower.includes('concept') || lower.includes('glossary') || lower.includes('term')) {
return {
type: NounType.Concept,
confidence: 0.85,
evidence: `Sheet name suggests concepts: "${sheetName}"`
}
}
return null
}
/**
* Get index size
*/
getIndexSize(): number {
return this.termIndex.size
}
/**
* Get statistics
*/
getStats() {
return {
...this.stats,
indexSize: this.termIndex.size,
cacheSize: this.cache.size,
cacheHitRate: this.stats.calls > 0 ? this.stats.cacheHits / this.stats.calls : 0,
termMatchRate: this.stats.calls > 0 ? this.stats.termMatches / this.stats.calls : 0,
metadataMatchRate: this.stats.calls > 0 ? this.stats.metadataMatches / this.stats.calls : 0,
formatMatchRate: this.stats.calls > 0 ? this.stats.formatMatches / this.stats.calls : 0
}
}
/**
* Reset statistics
*/
resetStats(): void {
this.stats = {
calls: 0,
cacheHits: 0,
termMatches: 0,
metadataMatches: 0,
formatMatches: 0
}
}
/**
* Clear cache
*/
clearCache(): void {
this.cache.clear()
this.cacheOrder = []
}
/**
* Clear index
*/
clearIndex(): void {
this.termIndex.clear()
}
// ========== Private Helper Methods ==========
/**
* Normalize text for matching
*/
private normalize(text: string): string {
return text.toLowerCase().trim()
}
/**
* Tokenize text into words
*/
private tokenize(text: string): string[] {
return text.toLowerCase().split(/\W+/).filter(t => t.length >= 3)
}
/**
* Generate cache key
*/
private getCacheKey(candidate: string, context?: any): string {
const normalized = this.normalize(candidate)
if (!context) return normalized
const parts = [normalized]
if (context.columnName) parts.push(context.columnName)
if (context.fileFormat) parts.push(context.fileFormat)
return parts.join(':')
}
/**
* Get from LRU cache
*/
private getFromCache(key: string): TypeSignal | null | undefined {
if (!this.cache.has(key)) return undefined
const cached = this.cache.get(key)
// Move to end (most recently used)
this.cacheOrder = this.cacheOrder.filter(k => k !== key)
this.cacheOrder.push(key)
return cached ?? null
}
/**
* Add to LRU cache with eviction
*/
private addToCache(key: string, value: TypeSignal | null): void {
this.cache.set(key, value)
this.cacheOrder.push(key)
// Evict oldest if over limit
if (this.cache.size > this.options.cacheSize) {
const oldest = this.cacheOrder.shift()
if (oldest) {
this.cache.delete(oldest)
}
}
}
}
/**
* Create a new ExactMatchSignal instance
*/
export function createExactMatchSignal(
brain: Brainy,
options?: ExactMatchSignalOptions
): ExactMatchSignal {
return new ExactMatchSignal(brain, options)
}