# Projection Strategy API ## Creating Custom Semantic Dimensions Projection strategies allow you to create custom ways to organize and access files in Semantic VFS. This guide shows you how to build your own. --- ## What is a Projection Strategy? A **projection strategy** maps a semantic dimension (like "priority" or "language") to actual file entities using Brainy queries. **Example:** ```typescript /by-priority/high → Files with metadata.priority = 'high' /by-language/typescript → Files with .ts extension ``` --- ## Interface Definition Every projection must implement the `ProjectionStrategy` interface: ```typescript export interface ProjectionStrategy { /** * Unique name for this dimension * Used in paths like: /by-{name}/... */ readonly name: string /** * Convert dimension value to Brainy FindParams * This is for documentation/debugging (not always used) * * @param value - The dimension value (e.g., 'high' for priority) * @param subpath - Optional file filter within dimension */ toQuery(value: any, subpath?: string): FindParams /** * Resolve dimension value to entity IDs * This is the MAIN method that does the work * * @param brain - Brainy instance (use brain.find, brain.similar, etc.) * @param vfs - VirtualFileSystem instance * @param value - The dimension value to resolve * @returns Array of entity IDs matching this dimension */ resolve(brain: Brainy, vfs: VirtualFileSystem, value: any): Promise /** * OPTIONAL: List all items in this dimension * Used for directory listings like: readdir('/by-priority') * * @param brain - Brainy instance * @param vfs - VirtualFileSystem instance * @param limit - Max results to return */ list?(brain: Brainy, vfs: VirtualFileSystem, limit?: number): Promise } ``` --- ## Quick Start: Priority Projection Let's build a projection that organizes files by priority (high, medium, low): ### Step 1: Create the Strategy Class ```typescript import { BaseProjectionStrategy } from '@soulcraft/brainy/vfs/semantic' import { Brainy } from '@soulcraft/brainy' import { VirtualFileSystem, VFSEntity } from '@soulcraft/brainy/vfs' export class PriorityProjection extends BaseProjectionStrategy { readonly name = 'priority' /** * Convert priority value to FindParams */ toQuery(priority: string, subpath?: string) { const query = { where: { vfsType: 'file', priority: priority // Match metadata.priority field }, limit: 1000 } // Filter by filename if subpath provided if (subpath) { query.where = { ...query.where, anyOf: [ { name: subpath }, { path: { endsWith: subpath } } ] } } return query } /** * Resolve priority to entity IDs */ async resolve(brain: Brainy, vfs: VirtualFileSystem, priority: string): Promise { // Query Brainy for files with this priority const results = await brain.find({ where: { vfsType: 'file', priority: priority }, limit: 1000 }) // Extract entity IDs using helper from base class return this.extractIds(results) } /** * List all files that have priority metadata */ async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise { const results = await brain.find({ where: { vfsType: 'file', priority: { exists: true } }, limit }) return results.map(r => r.entity as VFSEntity) } } ``` ### Step 2: Register the Strategy ```typescript import { Brainy } from '@soulcraft/brainy' import { PriorityProjection } from './PriorityProjection' const brain = new Brainy() await brain.init() const vfs = brain.vfs() await vfs.init() // Register custom projection // TODO: This will be exposed as public API // For now, access via internal property vfs['projectionRegistry'].register(new PriorityProjection()) ``` ### Step 3: Use It! ```typescript // Write files with priority metadata await vfs.writeFile('/src/critical-fix.ts', code, { metadata: { priority: 'high' } }) await vfs.writeFile('/src/nice-to-have.ts', code, { metadata: { priority: 'low' } }) // Access by priority const highPriority = await vfs.readdir('/by-priority/high') console.log(highPriority) // ['critical-fix.ts'] const lowPriority = await vfs.readdir('/by-priority/low') console.log(lowPriority) // ['nice-to-have.ts'] ``` --- ## Base Class Helpers `BaseProjectionStrategy` provides utility methods: ### `extractIds(results: Result[]): string[]` Extracts entity IDs from Brainy query results: ```typescript const results = await brain.find({ where: { ... } }) return this.extractIds(results) // ['id1', 'id2', ...] ``` ### `filterFiles(brain: Brainy, ids: string[]): Promise` Filters to only file entities (removes directories): ```typescript const allIds = await this.traverseGraph(...) return await this.filterFiles(brain, allIds) // Only files ``` --- ## Advanced Examples ### Example 1: Language Projection Organize files by programming language: ```typescript export class LanguageProjection extends BaseProjectionStrategy { readonly name = 'language' // Map extensions to languages private languageMap = { ts: 'typescript', js: 'javascript', py: 'python', go: 'go', rs: 'rust' } toQuery(language: string, subpath?: string) { // Find extension for this language const ext = Object.entries(this.languageMap) .find(([_, lang]) => lang === language)?.[0] return { where: { vfsType: 'file', extension: ext }, limit: 1000 } } async resolve(brain: Brainy, vfs: VirtualFileSystem, language: string): Promise { const ext = Object.entries(this.languageMap) .find(([_, lang]) => lang === language)?.[0] if (!ext) return [] const results = await brain.find({ where: { vfsType: 'file', extension: ext }, limit: 5000 }) return this.extractIds(results) } async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise { // Return sample files from each language const results = await brain.find({ where: { vfsType: 'file' }, limit }) return results.map(r => r.entity as VFSEntity) } } // Usage: // /by-language/typescript → All .ts files // /by-language/python → All .py files ``` ### Example 2: Size Projection Organize files by size category: ```typescript export class SizeProjection extends BaseProjectionStrategy { readonly name = 'size' // Size categories in bytes private readonly categories = { tiny: [0, 1024], // < 1 KB small: [1024, 102400], // 1-100 KB medium: [102400, 1048576], // 100 KB - 1 MB large: [1048576, Infinity] // > 1 MB } toQuery(category: string, subpath?: string) { const [min, max] = this.categories[category] || [0, Infinity] return { where: { vfsType: 'file', size: { gte: min, lessThan: max } }, limit: 1000 } } async resolve(brain: Brainy, vfs: VirtualFileSystem, category: string): Promise { const [min, max] = this.categories[category] if (!min && min !== 0) return [] const results = await brain.find({ where: { vfsType: 'file', size: { gte: min, lessThan: max } }, limit: 1000 }) return this.extractIds(results) } async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise { // Return files sorted by size const results = await brain.find({ where: { vfsType: 'file' }, limit }) return results .map(r => r.entity as VFSEntity) .sort((a, b) => (b.metadata.size || 0) - (a.metadata.size || 0)) } } // Usage: // /by-size/tiny → Files < 1 KB // /by-size/large → Files > 1 MB ``` ### Example 3: Status Projection (Custom Logic) Organize files by review status with custom logic: ```typescript export class StatusProjection extends BaseProjectionStrategy { readonly name = 'status' toQuery(status: string, subpath?: string) { return { where: { vfsType: 'file', reviewStatus: status }, limit: 1000 } } async resolve(brain: Brainy, vfs: VirtualFileSystem, status: string): Promise { // Custom logic: "needs-review" means modified in last 24h without review if (status === 'needs-review') { const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000) const results = await brain.find({ where: { vfsType: 'file', modified: { gte: oneDayAgo }, reviewStatus: { missing: true } // No review status set }, limit: 1000 }) return this.extractIds(results) } // Standard status query const results = await brain.find({ where: { vfsType: 'file', reviewStatus: status }, limit: 1000 }) return this.extractIds(results) } async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise { // Return files with any review status const results = await brain.find({ where: { vfsType: 'file', anyOf: [ { reviewStatus: { exists: true } }, { modified: { gte: Date.now() - 86400000 } } ] }, limit }) return results.map(r => r.entity as VFSEntity) } } // Usage: // /by-status/needs-review → Files modified in last 24h without review // /by-status/approved → Approved files // /by-status/rejected → Rejected files ``` --- ## Using Brainy Field Operators (BFO) Projection strategies use **Brainy Field Operators** (BFO), not MongoDB-style operators: ### Comparison Operators ```typescript // ❌ MongoDB style (WRONG) { size: { $gte: 1000, $lte: 5000 } } // ✅ BFO style (CORRECT) { size: { gte: 1000, lte: 5000 } } ``` ### Logical Operators ```typescript // ❌ MongoDB style (WRONG) { $or: [{ name: 'foo' }, { name: 'bar' }] } // ✅ BFO style (CORRECT) { anyOf: [{ name: 'foo' }, { name: 'bar' }] } ``` ### Existence Operators ```typescript // ❌ MongoDB style (WRONG) { tags: { $exists: true } } // ✅ BFO style (CORRECT) { tags: { exists: true } } ``` ### String Operators ```typescript // ❌ MongoDB style (WRONG) { path: { $regex: /\.ts$/ } } // ✅ BFO style (CORRECT) { path: { endsWith: '.ts' } } ``` ### Full BFO Operator Reference ```typescript // Comparison { field: value } // Exact match { field: { greaterThan: 10 } } // > { field: { gte: 10 } } // >= { field: { lessThan: 10 } } // < { field: { lte: 10 } } // <= { field: { not: value } } // != // Logical { anyOf: [{ a: 1 }, { b: 2 }] } // OR { allOf: [{ a: 1 }, { b: 2 }] } // AND // Existence { field: { exists: true } } // Field exists { field: { missing: true } } // Field doesn't exist // String { field: { startsWith: 'prefix' } } // Starts with { field: { endsWith: 'suffix' } } // Ends with { field: { matches: 'pattern' } } // Regex match // Array { array: { contains: 'item' } } // Array contains item { array: { hasAll: ['a', 'b'] } } // Has all items { array: { oneOf: ['a', 'b', 'c'] } } // Value in list ``` --- ## Performance Guidelines ### 1. Use Indexes All metadata fields are automatically indexed. Use direct equality or range queries for best performance: ```typescript // ✅ Fast: Direct index lookup (O(log n)) { priority: 'high' } { size: { gte: 1000 } } // ⚠️ Slower: Must scan results { path: { matches: /complex-regex/ } } ``` ### 2. Limit Results Always set reasonable limits: ```typescript async resolve(brain, vfs, value) { const results = await brain.find({ where: { ... }, limit: 1000 // Prevent unbounded queries }) return this.extractIds(results) } ``` ### 3. Avoid Post-Filtering When Possible If you need post-filtering, consider flattening data: ```typescript // ❌ Slow: Fetch 5000, filter in memory const all = await brain.find({ where: { type: 'file' }, limit: 5000 }) return all.filter(item => item.metadata.nested.value === target) // ✅ Fast: Flatten during write, query directly // Store: metadata.nested_value = target const results = await brain.find({ where: { nested_value: target }, limit: 1000 }) ``` ### 4. Cache Expensive Operations Use the projection's resolve cache: ```typescript // Automatic caching in SemanticPathResolver // Results cached for 5 minutes by default // No manual caching needed! ``` --- ## Testing Projections ### Unit Test Example ```typescript import { describe, it, expect, beforeAll } from 'vitest' import { Brainy } from '@soulcraft/brainy' import { PriorityProjection } from './PriorityProjection' describe('PriorityProjection', () => { let brain: Brainy let vfs: any let projection: PriorityProjection beforeAll(async () => { brain = new Brainy() await brain.init() vfs = brain.vfs() await vfs.init() projection = new PriorityProjection() }) it('should resolve high priority files', async () => { // Create test files await vfs.writeFile('/test1.ts', 'code', { metadata: { priority: 'high' } }) await vfs.writeFile('/test2.ts', 'code', { metadata: { priority: 'low' } }) // Resolve high priority const ids = await projection.resolve(brain, vfs, 'high') expect(ids).toHaveLength(1) const entity = await brain.get(ids[0]) expect(entity.metadata.priority).toBe('high') }) it('should list all files with priority', async () => { const entities = await projection.list(brain, vfs, 100) expect(entities.length).toBeGreaterThan(0) expect(entities.every(e => e.metadata.priority)).toBe(true) }) }) ``` --- ## Best Practices ### 1. ✅ Name projections clearly ```typescript // ✅ Good readonly name = 'priority' // /by-priority/high readonly name = 'language' // /by-language/typescript // ❌ Bad readonly name = 'proj1' // /by-proj1/??? unclear ``` ### 2. ✅ Document expected metadata ```typescript /** * Priority Projection * * Requires metadata fields: * - priority: string ('high' | 'medium' | 'low') * * Usage: * /by-priority/high */ export class PriorityProjection extends BaseProjectionStrategy { // ... } ``` ### 3. ✅ Handle missing data gracefully ```typescript async resolve(brain, vfs, value) { const results = await brain.find({ where: { priority: value }, limit: 1000 }) // Return empty array if no results, don't throw return this.extractIds(results) // [] if empty } ``` ### 4. ✅ Validate input ```typescript async resolve(brain, vfs, priority: string) { // Validate priority value const valid = ['high', 'medium', 'low'] if (!valid.includes(priority)) { return [] // Or throw error } // Continue with query... } ``` --- ## Common Patterns ### Pattern 1: Enum-Based Projection For fixed sets of values (status, priority, type): ```typescript private readonly validValues = ['draft', 'review', 'approved'] async resolve(brain, vfs, status: string) { if (!this.validValues.includes(status)) return [] // ... query } ``` ### Pattern 2: Range-Based Projection For numeric or time ranges: ```typescript private readonly ranges = { recent: Date.now() - 86400000, // Last 24h week: Date.now() - 7 * 86400000, // Last week month: Date.now() - 30 * 86400000 // Last month } async resolve(brain, vfs, period: string) { const since = this.ranges[period] if (!since) return [] const results = await brain.find({ where: { modified: { gte: since } } }) return this.extractIds(results) } ``` ### Pattern 3: Computed Projection Combine multiple criteria: ```typescript async resolve(brain, vfs, value: string) { // "stale" = not modified in 30 days AND no recent access if (value === 'stale') { const thirtyDaysAgo = Date.now() - 30 * 86400000 const results = await brain.find({ where: { allOf: [ { modified: { lessThan: thirtyDaysAgo } }, { accessed: { lessThan: thirtyDaysAgo } } ] } }) return this.extractIds(results) } // Regular query for other values... } ``` --- ## Troubleshooting ### Projection returns empty results 1. Check metadata exists: `console.log(entity.metadata)` 2. Verify query syntax: Use BFO operators, not MongoDB 3. Check limits: Increase limit if needed ### Slow performance 1. Check if field is indexed: All metadata fields are auto-indexed 2. Avoid post-filtering: Flatten complex structures 3. Use appropriate limits: Don't fetch more than needed ### Type errors 1. Import correct types: `import { Brainy, VirtualFileSystem } from '@soulcraft/brainy'` 2. Use `as VFSEntity` when mapping results 3. Check BaseProjectionStrategy import --- ## See Also - [Semantic VFS Guide](./SEMANTIC_VFS.md) - Using semantic paths - [Performance Tuning](./PERFORMANCE_TUNING.md) - Optimization guide - [VFS Core API](./VFS_CORE.md) - Base VFS operations