2025-10-22 17:36:27 -07:00
|
|
|
/**
|
|
|
|
|
* 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
|
2025-10-22 17:36:27 -07:00
|
|
|
* 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.)
|
2025-10-22 17:36:27 -07:00
|
|
|
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')
|
|
|
|
|
}
|