brainy/src/neural/presets.ts

480 lines
12 KiB
TypeScript
Raw Normal View History

/**
* Smart Import Presets - Zero-Configuration Auto-Detection
*
* Automatically selects optimal import strategy based on:
* - File type (Excel, CSV, PDF, Markdown, JSON)
* - File size and row count
* - Column structure (explicit relationships vs narrative)
* - Available memory and performance requirements
*
* Production-ready: Handles billions of entities with optimal performance
*/
import { NounType, VerbType } from '../types/graphTypes.js'
/**
* Signal types used for entity classification
*/
export type SignalType = 'embedding' | 'exact' | 'pattern' | 'context'
/**
* Strategy types used for relationship extraction
*/
export type StrategyType = 'explicit' | 'pattern' | 'embedding'
/**
* Import context for preset auto-detection
*/
export interface ImportContext {
fileType?: 'excel' | 'csv' | 'json' | 'pdf' | 'markdown' | 'unknown'
fileSize?: number // bytes
rowCount?: number
hasExplicitColumns?: boolean // Has "Related Terms" or similar columns
hasNarrativeContent?: boolean // Has long-form text/descriptions
avgDefinitionLength?: number // Average length of definitions
memoryAvailable?: number // bytes
}
/**
* Signal configuration with weights
*/
export interface SignalConfig {
enabled: SignalType[]
weights: Record<SignalType, number>
timeout: number // milliseconds
}
/**
* Strategy configuration with priorities
*/
export interface StrategyConfig {
enabled: StrategyType[]
timeout: number // milliseconds
earlyTermination: boolean
minConfidence: number
}
/**
* Complete preset configuration
*/
export interface PresetConfig {
name: string
description: string
signals: SignalConfig
strategies: StrategyConfig
streaming: boolean
batchSize: number
}
/**
* Fast Preset - For large imports (>10K rows)
*
* Optimized for speed over accuracy:
* - Only exact match and pattern signals
* - Only explicit strategy (O(1) lookups)
* - Streaming enabled for memory efficiency
* - Early termination on first high-confidence match
*
* Use case: Bulk imports, data migrations
* Performance: ~10ms per row
* Accuracy: ~85%
*/
export const FAST_PRESET: PresetConfig = {
name: 'fast',
description: 'Fast bulk import for large datasets',
signals: {
enabled: ['exact', 'pattern'],
weights: {
exact: 0.70,
pattern: 0.30,
embedding: 0,
context: 0
},
timeout: 50
},
strategies: {
enabled: ['explicit'],
timeout: 100,
earlyTermination: true,
minConfidence: 0.70
},
streaming: true,
batchSize: 1000
}
/**
* Balanced Preset - Default for most imports
*
* Good balance of speed and accuracy:
* - All signals except context (embedding, exact, pattern)
* - All strategies with smart ordering
* - Moderate timeouts
* - Early termination after high-confidence matches
*
* Use case: Standard imports, general glossaries
* Performance: ~30ms per row
* Accuracy: ~92%
*/
export const BALANCED_PRESET: PresetConfig = {
name: 'balanced',
description: 'Balanced speed and accuracy for most imports',
signals: {
enabled: ['exact', 'embedding', 'pattern'],
weights: {
exact: 0.40,
embedding: 0.35,
pattern: 0.25,
context: 0
},
timeout: 100
},
strategies: {
enabled: ['explicit', 'pattern', 'embedding'],
timeout: 200,
earlyTermination: true,
minConfidence: 0.65
},
streaming: false,
batchSize: 500
}
/**
* Accurate Preset - For small, critical imports
*
* Optimized for accuracy over speed:
* - All signals including context
* - All strategies, no early termination
* - Longer timeouts for thorough analysis
* - Lower confidence threshold (accept more matches)
*
* Use case: Knowledge bases, critical taxonomies
* Performance: ~100ms per row
* Accuracy: ~97%
*/
export const ACCURATE_PRESET: PresetConfig = {
name: 'accurate',
description: 'Maximum accuracy for critical imports',
signals: {
enabled: ['exact', 'embedding', 'pattern', 'context'],
weights: {
exact: 0.40,
embedding: 0.35,
pattern: 0.20,
context: 0.05
},
timeout: 500
},
strategies: {
enabled: ['explicit', 'pattern', 'embedding'],
timeout: 1000,
earlyTermination: false,
minConfidence: 0.50
},
streaming: false,
batchSize: 100
}
/**
* Explicit Preset - For glossaries with relationship columns
*
* Optimized for structured data with explicit relationships:
* - Only exact match signals (no AI needed)
* - Only explicit and pattern strategies
* - Fast, deterministic results
* - Perfect for Excel/CSV with "Related Terms" columns
*
fix: recalibrate find({ limit }) cap + two-tier enforcement + caller location Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })` to prevent OOM. The cap was sound in intent but ~4x too conservative in calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB (384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB free-memory box the cap derived to 9000 — breaking common safety-cap patterns like `find({ type, where, limit: 10_000 })` that typically return 10-500 entities. Surfaced as a runtime regression with cascading 500s degrading production dashboards. Three concurrent fixes: A. RECALIBRATE THE FORMULA - src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities (reservedQueryMemory / containerMemory / freeMemory) all divided by 100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed entity size. - Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 → 20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides unchanged in behavior. B. TWO-TIER ENFORCEMENT (warn-then-throw) - Below cap (limit <= maxLimit): silent pass, unchanged. - Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per call site (dedup keyed on caller stack frame + limit value), query proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical safety-cap limits keeps working; the warning teaches the recipe so consumers can fix it intentionally. - Hard tier (limit > 2 * maxLimit): throw with the same teaching message format. Real OOM territory; the cap stops being a recommendation and becomes a guardrail. - The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000 against a 9 K-cap box) without disabling OOM protection. Real OOM territory on a JS in-memory brain is hundreds of thousands of results, not 10x the safety cap. C. IMPROVED ERROR / WARNING MESSAGE - Same shape as the 7.30.1 enforcement-error messages: state the problem, name the three escape valves (maxQueryLimit / reservedQueryMemory / pagination), include caller location, link to docs. - Extracted findCallerLocation() helper from brainy.ts to a new src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and the limit enforcement (7.30.2) share one implementation without circular imports. DOCS - New docs/guides/find-limits.md (public: true) — full reference: why the cap exists, the four memory sources the auto-config considers, the three escape valves with when-to-use-which guidance, and an explicit "pagination is the future-proof pattern" callout (8.0 may tighten the cap further; pagination keeps working unchanged). - docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer to the new guide. - RELEASES.md v7.30.2 entry. TESTS - New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass; soft-tier warns once per call site (dedup verified by exercising same vs. different source lines via wrapper closures); soft-tier message format (names all three escape valves + docs link); soft-tier message includes caller location; hard-tier throws; hard-tier message format same as soft-tier; consumer maxQueryLimit override raises the cap and shifts both tiers accordingly; pre-7.30.2 regression scenario explicitly covered. - tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result assumption). - tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover the three-tier semantics (below-cap pass / soft-tier silent / hard-tier throw). - Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and- enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468. CORTEX COMPATIBILITY - Zero Cortex changes required. Every change is JS-side: formula recalibration runs in ValidationConfig.constructor(), two-tier enforcement runs in validateFindParams(), both fire before any storage / index / Cortex call. - The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten per-call limits to keep snapshot semantics cheap; pagination remains the pattern that's guaranteed to keep working. REPO-WIDE CLEANUP Brainy is the only Soulcraft project that is open source. This commit also scrubs closed-source product names and product-specific class/field references from every tracked file in the repo (src/, docs/, tests/, RELEASES.md, CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release notes now refer to "a consumer", "a downstream application", "a production deployment", or "an internal report" — never to the named product. Two product-named test files renamed to neutral diagnostic names. CLAUDE.md gains a project-level guard rule documenting the policy and an example list of the identifiers that may not appear in tracked code. Verification - npx tsc --noEmit: clean - npm test: 1468 / 1468 unit - All four integration subtype + verb + strict + find-limits suites: 78/78 - npm run build: clean - Closed-source product reference audit: clean
2026-06-08 12:34:05 -07:00
* Use case: structured taxonomies and glossaries from spreadsheet sources
* Performance: ~5ms per row
* Accuracy: ~99% (high confidence)
*/
export const EXPLICIT_PRESET: PresetConfig = {
name: 'explicit',
description: 'For glossaries with explicit relationship columns',
signals: {
enabled: ['exact', 'pattern'],
weights: {
exact: 0.70,
pattern: 0.30,
embedding: 0,
context: 0
},
timeout: 50
},
strategies: {
enabled: ['explicit', 'pattern'],
timeout: 100,
earlyTermination: true,
minConfidence: 0.80
},
streaming: false,
batchSize: 500
}
/**
* Pattern Preset - For documents with narrative content
*
* Optimized for unstructured text with rich patterns:
* - Embedding and pattern signals (semantic understanding)
* - Pattern and embedding strategies
* - Good for PDFs, articles, documentation
*
* Use case: PDF imports, markdown docs, articles
* Performance: ~50ms per row
* Accuracy: ~90%
*/
export const PATTERN_PRESET: PresetConfig = {
name: 'pattern',
description: 'For documents with narrative content',
signals: {
enabled: ['embedding', 'pattern', 'context'],
weights: {
embedding: 0.50,
pattern: 0.40,
context: 0.10,
exact: 0
},
timeout: 200
},
strategies: {
enabled: ['pattern', 'embedding'],
timeout: 300,
earlyTermination: false,
minConfidence: 0.60
},
streaming: false,
batchSize: 200
}
/**
* All available presets
*/
export const PRESETS: Record<string, PresetConfig> = {
fast: FAST_PRESET,
balanced: BALANCED_PRESET,
accurate: ACCURATE_PRESET,
explicit: EXPLICIT_PRESET,
pattern: PATTERN_PRESET
}
/**
* Auto-detect optimal preset based on import context
*
* Decision tree:
* 1. Large dataset (>10K rows or >10MB) fast
* 2. Small dataset (<100 rows) accurate
* 3. Excel/CSV with explicit columns explicit
* 4. PDF/Markdown with long content pattern
* 5. Default balanced
*
* @param context Import context (file type, size, structure)
* @returns Optimal preset configuration
*/
export function autoDetectPreset(context: ImportContext = {}): PresetConfig {
const {
fileType = 'unknown',
fileSize = 0,
rowCount = 0,
hasExplicitColumns = false,
hasNarrativeContent = false,
avgDefinitionLength = 0
} = context
// Rule 1: Large imports → fast preset (prioritize speed)
if (rowCount > 10000 || fileSize > 10_000_000) {
return FAST_PRESET
}
// Rule 2: Small critical imports → accurate preset (prioritize accuracy)
if (rowCount > 0 && rowCount < 100) {
return ACCURATE_PRESET
}
// Rule 3: Structured data with explicit relationships → explicit preset
fix: recalibrate find({ limit }) cap + two-tier enforcement + caller location Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })` to prevent OOM. The cap was sound in intent but ~4x too conservative in calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB (384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB free-memory box the cap derived to 9000 — breaking common safety-cap patterns like `find({ type, where, limit: 10_000 })` that typically return 10-500 entities. Surfaced as a runtime regression with cascading 500s degrading production dashboards. Three concurrent fixes: A. RECALIBRATE THE FORMULA - src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities (reservedQueryMemory / containerMemory / freeMemory) all divided by 100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed entity size. - Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 → 20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides unchanged in behavior. B. TWO-TIER ENFORCEMENT (warn-then-throw) - Below cap (limit <= maxLimit): silent pass, unchanged. - Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per call site (dedup keyed on caller stack frame + limit value), query proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical safety-cap limits keeps working; the warning teaches the recipe so consumers can fix it intentionally. - Hard tier (limit > 2 * maxLimit): throw with the same teaching message format. Real OOM territory; the cap stops being a recommendation and becomes a guardrail. - The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000 against a 9 K-cap box) without disabling OOM protection. Real OOM territory on a JS in-memory brain is hundreds of thousands of results, not 10x the safety cap. C. IMPROVED ERROR / WARNING MESSAGE - Same shape as the 7.30.1 enforcement-error messages: state the problem, name the three escape valves (maxQueryLimit / reservedQueryMemory / pagination), include caller location, link to docs. - Extracted findCallerLocation() helper from brainy.ts to a new src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and the limit enforcement (7.30.2) share one implementation without circular imports. DOCS - New docs/guides/find-limits.md (public: true) — full reference: why the cap exists, the four memory sources the auto-config considers, the three escape valves with when-to-use-which guidance, and an explicit "pagination is the future-proof pattern" callout (8.0 may tighten the cap further; pagination keeps working unchanged). - docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer to the new guide. - RELEASES.md v7.30.2 entry. TESTS - New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass; soft-tier warns once per call site (dedup verified by exercising same vs. different source lines via wrapper closures); soft-tier message format (names all three escape valves + docs link); soft-tier message includes caller location; hard-tier throws; hard-tier message format same as soft-tier; consumer maxQueryLimit override raises the cap and shifts both tiers accordingly; pre-7.30.2 regression scenario explicitly covered. - tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result assumption). - tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover the three-tier semantics (below-cap pass / soft-tier silent / hard-tier throw). - Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and- enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468. CORTEX COMPATIBILITY - Zero Cortex changes required. Every change is JS-side: formula recalibration runs in ValidationConfig.constructor(), two-tier enforcement runs in validateFindParams(), both fire before any storage / index / Cortex call. - The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten per-call limits to keep snapshot semantics cheap; pagination remains the pattern that's guaranteed to keep working. REPO-WIDE CLEANUP Brainy is the only Soulcraft project that is open source. This commit also scrubs closed-source product names and product-specific class/field references from every tracked file in the repo (src/, docs/, tests/, RELEASES.md, CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release notes now refer to "a consumer", "a downstream application", "a production deployment", or "an internal report" — never to the named product. Two product-named test files renamed to neutral diagnostic names. CLAUDE.md gains a project-level guard rule documenting the policy and an example list of the identifiers that may not appear in tracked code. Verification - npx tsc --noEmit: clean - npm test: 1468 / 1468 unit - All four integration subtype + verb + strict + find-limits suites: 78/78 - npm run build: clean - Closed-source product reference audit: clean
2026-06-08 12:34:05 -07:00
// (Handles spreadsheet imports where relationships are encoded in columns.)
if (hasExplicitColumns && (fileType === 'excel' || fileType === 'csv')) {
return EXPLICIT_PRESET
}
// Rule 4: Narrative content → pattern preset
// Good for PDFs, articles, documentation
if (
hasNarrativeContent ||
fileType === 'pdf' ||
fileType === 'markdown' ||
avgDefinitionLength > 500
) {
return PATTERN_PRESET
}
// Rule 5: JSON data → balanced preset
if (fileType === 'json') {
return BALANCED_PRESET
}
// Default: balanced preset
return BALANCED_PRESET
}
/**
* Get preset by name
*
* @param name Preset name (fast, balanced, accurate, explicit, pattern)
* @returns Preset configuration
* @throws Error if preset not found
*/
export function getPreset(name: string): PresetConfig {
const preset = PRESETS[name.toLowerCase()]
if (!preset) {
throw new Error(`Unknown preset: ${name}. Available: ${Object.keys(PRESETS).join(', ')}`)
}
return preset
}
/**
* Get all available preset names
*
* @returns Array of preset names
*/
export function getPresetNames(): string[] {
return Object.keys(PRESETS)
}
/**
* Explain why a preset was selected
*
* @param context Import context
* @returns Human-readable explanation
*/
export function explainPresetChoice(context: ImportContext = {}): string {
const {
fileType = 'unknown',
fileSize = 0,
rowCount = 0,
hasExplicitColumns = false,
hasNarrativeContent = false,
avgDefinitionLength = 0
} = context
if (rowCount > 10000 || fileSize > 10_000_000) {
return `Large dataset (${rowCount} rows, ${(fileSize / 1_000_000).toFixed(1)}MB) → fast preset for optimal performance`
}
if (rowCount > 0 && rowCount < 100) {
return `Small critical dataset (${rowCount} rows) → accurate preset for maximum accuracy`
}
if (hasExplicitColumns && (fileType === 'excel' || fileType === 'csv')) {
return `${fileType.toUpperCase()} with explicit relationship columns → explicit preset for deterministic results`
}
if (hasNarrativeContent || fileType === 'pdf' || fileType === 'markdown') {
return `Narrative content (${fileType}) → pattern preset for semantic understanding`
}
if (fileType === 'json') {
return `JSON data → balanced preset for structured imports`
}
return `Standard import → balanced preset (default)`
}
/**
* Create custom preset by merging with base preset
*
* @param baseName Base preset name
* @param overrides Custom overrides
* @returns Custom preset configuration
*/
export function createCustomPreset(
baseName: string,
overrides: Partial<PresetConfig>
): PresetConfig {
const base = getPreset(baseName)
return {
...base,
...overrides,
signals: {
...base.signals,
...(overrides.signals || {})
},
strategies: {
...base.strategies,
...(overrides.strategies || {})
}
}
}
/**
* Validate preset configuration
*
* @param preset Preset to validate
* @returns True if valid, throws error otherwise
*/
export function validatePreset(preset: PresetConfig): boolean {
// Validate signals
if (preset.signals.enabled.length === 0) {
throw new Error('Preset must have at least one enabled signal')
}
// Validate strategies
if (preset.strategies.enabled.length === 0) {
throw new Error('Preset must have at least one enabled strategy')
}
// Validate weights sum to ~1.0
const enabledSignals = preset.signals.enabled
const totalWeight = enabledSignals.reduce(
(sum, signal) => sum + preset.signals.weights[signal],
0
)
if (Math.abs(totalWeight - 1.0) > 0.01) {
throw new Error(
`Signal weights must sum to 1.0, got ${totalWeight.toFixed(2)}`
)
}
// Validate timeouts
if (preset.signals.timeout <= 0 || preset.strategies.timeout <= 0) {
throw new Error('Timeouts must be positive')
}
// Validate batch size
if (preset.batchSize <= 0) {
throw new Error('Batch size must be positive')
}
return true
}
/**
* Format preset for display
*
* @param preset Preset configuration
* @returns Human-readable preset summary
*/
export function formatPreset(preset: PresetConfig): string {
const lines = [
`Preset: ${preset.name}`,
`Description: ${preset.description}`,
'',
'Signals:',
...preset.signals.enabled.map(
(s) => ` - ${s}: ${(preset.signals.weights[s] * 100).toFixed(0)}%`
),
` Timeout: ${preset.signals.timeout}ms`,
'',
'Strategies:',
...preset.strategies.enabled.map((s) => ` - ${s}`),
` Timeout: ${preset.strategies.timeout}ms`,
` Early termination: ${preset.strategies.earlyTermination}`,
` Min confidence: ${preset.strategies.minConfidence}`,
'',
`Streaming: ${preset.streaming}`,
`Batch size: ${preset.batchSize}`
]
return lines.join('\n')
}