feat: add neural extraction APIs with NounType taxonomy

Add brain.extract() and brain.extractConcepts() methods that use
NeuralEntityExtractor with embeddings and sophisticated NounType
taxonomy (30+ entity types) for semantic entity and concept extraction.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-09-29 13:51:47 -07:00
parent 27cc699555
commit dd50d89ad6
41 changed files with 3807 additions and 7391 deletions

View file

@ -0,0 +1,147 @@
/**
* Projection Registry
*
* Central registry for all projection strategies
* Manages strategy lookup and execution
*/
import { ProjectionStrategy } from './ProjectionStrategy.js'
import { Brainy } from '../../brainy.js'
import { VirtualFileSystem } from '../VirtualFileSystem.js'
import { VFSEntity } from '../types.js'
/**
* Registry for projection strategies
* Allows dynamic registration and lookup of strategies
*/
export class ProjectionRegistry {
private strategies = new Map<string, ProjectionStrategy>()
/**
* Register a projection strategy
* @param strategy - The strategy to register
* @throws Error if strategy with same name already registered
*/
register(strategy: ProjectionStrategy): void {
if (this.strategies.has(strategy.name)) {
throw new Error(`Projection strategy '${strategy.name}' is already registered`)
}
this.strategies.set(strategy.name, strategy)
}
/**
* Get a projection strategy by name
* @param name - Strategy name
* @returns The strategy or undefined if not found
*/
get(name: string): ProjectionStrategy | undefined {
return this.strategies.get(name)
}
/**
* Check if a strategy is registered
* @param name - Strategy name
*/
has(name: string): boolean {
return this.strategies.has(name)
}
/**
* List all registered strategy names
*/
listDimensions(): string[] {
return Array.from(this.strategies.keys())
}
/**
* Get count of registered strategies
*/
count(): number {
return this.strategies.size
}
/**
* Resolve a dimension value to entity IDs
* Convenience method that looks up strategy and calls resolve()
*
* @param dimension - The semantic dimension
* @param value - The value to resolve
* @param brain - REAL Brainy instance
* @param vfs - REAL VirtualFileSystem instance
* @returns Array of entity IDs
* @throws Error if dimension not registered
*/
async resolve(
dimension: string,
value: any,
brain: Brainy,
vfs: VirtualFileSystem
): Promise<string[]> {
const strategy = this.get(dimension)
if (!strategy) {
throw new Error(`Unknown projection dimension: ${dimension}. Registered dimensions: ${this.listDimensions().join(', ')}`)
}
// Call REAL strategy resolve method
return await strategy.resolve(brain, vfs, value)
}
/**
* List entities in a dimension
* Convenience method for strategies that support listing
*
* @param dimension - The semantic dimension
* @param brain - REAL Brainy instance
* @param vfs - REAL VirtualFileSystem instance
* @param limit - Max results
* @returns Array of VFSEntity
* @throws Error if dimension not registered or doesn't support listing
*/
async list(
dimension: string,
brain: Brainy,
vfs: VirtualFileSystem,
limit?: number
): Promise<VFSEntity[]> {
const strategy = this.get(dimension)
if (!strategy) {
throw new Error(`Unknown projection dimension: ${dimension}`)
}
if (!strategy.list) {
throw new Error(`Projection '${dimension}' does not support listing`)
}
return await strategy.list(brain, vfs, limit)
}
/**
* Unregister a strategy
* Useful for testing or dynamic strategy management
*
* @param name - Strategy name to remove
* @returns true if removed, false if not found
*/
unregister(name: string): boolean {
return this.strategies.delete(name)
}
/**
* Clear all registered strategies
* Useful for testing
*/
clear(): void {
this.strategies.clear()
}
/**
* Get all registered strategies
* Returns a copy to prevent external modification
*/
getAll(): ProjectionStrategy[] {
return Array.from(this.strategies.values())
}
}

View file

@ -0,0 +1,93 @@
/**
* Projection Strategy Interface
*
* Defines how to map semantic path dimensions to Brainy queries
* Each strategy uses EXISTING Brainy indexes and methods
*/
import { Brainy } from '../../brainy.js'
import { VirtualFileSystem } from '../VirtualFileSystem.js'
import { FindParams } from '../../types/brainy.types.js'
import { VFSEntity } from '../types.js'
/**
* Strategy for projecting semantic paths into entity queries
* All implementations MUST use real Brainy methods (no stubs!)
*/
export interface ProjectionStrategy {
/**
* Strategy name (used for registration)
*/
readonly name: string
/**
* Convert semantic value to Brainy FindParams
* Uses EXISTING FindParams type from brainy.types.ts
*/
toQuery(value: any, subpath?: string): FindParams
/**
* Resolve semantic value to entity IDs
* Uses REAL Brainy.find() method
*
* @param brain - REAL Brainy instance
* @param vfs - REAL VirtualFileSystem instance
* @param value - The semantic value to resolve
* @returns Array of entity IDs that match
*/
resolve(brain: Brainy, vfs: VirtualFileSystem, value: any): Promise<string[]>
/**
* List all entities in this dimension
* Optional - not all strategies need to implement
*
* @param brain - REAL Brainy instance
* @param vfs - REAL VirtualFileSystem instance
* @param limit - Max results to return
*/
list?(brain: Brainy, vfs: VirtualFileSystem, limit?: number): Promise<VFSEntity[]>
}
/**
* Base class for projection strategies with common utilities
*/
export abstract class BaseProjectionStrategy implements ProjectionStrategy {
abstract readonly name: string
abstract toQuery(value: any, subpath?: string): FindParams
abstract resolve(brain: Brainy, vfs: VirtualFileSystem, value: any): Promise<string[]>
/**
* Convert Brainy Results to entity IDs
* Helper method for subclasses
*/
protected extractIds(results: Array<{ id: string }>): string[] {
return results.map(r => r.id)
}
/**
* Verify that an entity is a file (not directory)
* Uses REAL Brainy.get() method
*/
protected async isFile(brain: Brainy, entityId: string): Promise<boolean> {
const entity = await brain.get(entityId)
return entity?.metadata?.vfsType === 'file'
}
/**
* Filter entity IDs to only include files
* Uses REAL Brainy.get() for each entity
*/
protected async filterFiles(brain: Brainy, entityIds: string[]): Promise<string[]> {
const files: string[] = []
for (const id of entityIds) {
if (await this.isFile(brain, id)) {
files.push(id)
}
}
return files
}
}

View file

@ -0,0 +1,363 @@
/**
* Semantic Path Parser
*
* Parses semantic filesystem paths into structured queries
* PURE LOGIC - No external dependencies, no async operations
*
* Supported path formats:
* - Traditional: /src/auth.ts
* - By Concept: /by-concept/authentication/login.ts
* - By Author: /by-author/alice/file.ts
* - By Time: /as-of/2024-03-15/file.ts
* - By Relationship: /related-to/src/auth.ts/depth-2
* - By Similarity: /similar-to/src/auth.ts/threshold-0.8
* - By Tag: /by-tag/security/file.ts
*/
export type SemanticDimension =
| 'traditional'
| 'concept'
| 'author'
| 'time'
| 'relationship'
| 'similar'
| 'tag'
export interface ParsedSemanticPath {
dimension: SemanticDimension
value: string | Date | RelationshipValue | SimilarityValue
subpath?: string
filters?: Record<string, any>
}
export interface RelationshipValue {
targetPath: string
depth?: number
relationshipTypes?: string[]
}
export interface SimilarityValue {
targetPath: string
threshold?: number
}
/**
* Semantic Path Parser
* Parses various semantic path formats into structured data
*/
export class SemanticPathParser {
// Regex patterns for each dimension
private static readonly PATTERNS = {
concept: /^\/by-concept\/([^\/]+)(?:\/(.+))?$/,
author: /^\/by-author\/([^\/]+)(?:\/(.+))?$/,
time: /^\/as-of\/(\d{4}-\d{2}-\d{2})(?:\/(.+))?$/,
// Relationship: /related-to/<path>/depth-N/types-X,Y/<subpath>
// Must handle paths with slashes, so capture everything before /depth- or /types-
relationship: /^\/related-to\/(.+?)(?:\/depth-(\d+)|\/types-([^\/]+)|\/(.+))*$/,
// Similarity: /similar-to/<path>/threshold-N/<subpath>
similar: /^\/similar-to\/(.+?)(?:\/threshold-([\d.]+)|\/(.+))*$/,
tag: /^\/by-tag\/([^\/]+)(?:\/(.+))?$/
}
/**
* Parse a path into semantic components
* PURE FUNCTION - no external calls, no async
*/
parse(path: string): ParsedSemanticPath {
if (!path || typeof path !== 'string') {
throw new Error('Path must be a non-empty string')
}
// Normalize path
const normalized = this.normalizePath(path)
// Try concept dimension
const conceptMatch = normalized.match(SemanticPathParser.PATTERNS.concept)
if (conceptMatch) {
return {
dimension: 'concept',
value: conceptMatch[1],
subpath: conceptMatch[2]
}
}
// Try author dimension
const authorMatch = normalized.match(SemanticPathParser.PATTERNS.author)
if (authorMatch) {
return {
dimension: 'author',
value: authorMatch[1],
subpath: authorMatch[2]
}
}
// Try time dimension
const timeMatch = normalized.match(SemanticPathParser.PATTERNS.time)
if (timeMatch) {
const dateStr = timeMatch[1]
const date = this.parseDate(dateStr)
return {
dimension: 'time',
value: date,
subpath: timeMatch[2]
}
}
// Try relationship dimension
if (normalized.startsWith('/related-to/')) {
return this.parseRelationshipPath(normalized)
}
// Try similarity dimension
if (normalized.startsWith('/similar-to/')) {
return this.parseSimilarityPath(normalized)
}
// Try tag dimension
const tagMatch = normalized.match(SemanticPathParser.PATTERNS.tag)
if (tagMatch) {
return {
dimension: 'tag',
value: tagMatch[1],
subpath: tagMatch[2]
}
}
// Default to traditional path
return {
dimension: 'traditional',
value: normalized
}
}
/**
* Check if a path is semantic (non-traditional)
*/
isSemanticPath(path: string): boolean {
if (!path || typeof path !== 'string') {
return false
}
const normalized = this.normalizePath(path)
// Check if matches any semantic pattern
return (
normalized.startsWith('/by-concept/') ||
normalized.startsWith('/by-author/') ||
normalized.startsWith('/as-of/') ||
normalized.startsWith('/related-to/') ||
normalized.startsWith('/similar-to/') ||
normalized.startsWith('/by-tag/')
)
}
/**
* Get the dimension type from a path
*/
getDimension(path: string): SemanticDimension {
return this.parse(path).dimension
}
/**
* Normalize a path - remove trailing slashes, collapse multiple slashes
* PURE FUNCTION
*/
private normalizePath(path: string): string {
// Remove trailing slash (except for root)
let normalized = path.replace(/\/+$/, '')
// Collapse multiple slashes
normalized = normalized.replace(/\/+/g, '/')
// Ensure starts with /
if (!normalized.startsWith('/')) {
normalized = '/' + normalized
}
// Special case: empty path becomes /
if (normalized === '') {
normalized = '/'
}
return normalized
}
/**
* Parse date string (YYYY-MM-DD) into Date object
* PURE FUNCTION
*/
private parseDate(dateStr: string): Date {
const parts = dateStr.split('-')
if (parts.length !== 3) {
throw new Error(`Invalid date format: ${dateStr}. Expected YYYY-MM-DD`)
}
const year = parseInt(parts[0], 10)
const month = parseInt(parts[1], 10) - 1 // Months are 0-indexed in JS
const day = parseInt(parts[2], 10)
if (isNaN(year) || isNaN(month) || isNaN(day)) {
throw new Error(`Invalid date format: ${dateStr}. Expected YYYY-MM-DD`)
}
if (year < 1900 || year > 2100) {
throw new Error(`Invalid year: ${year}. Expected 1900-2100`)
}
if (month < 0 || month > 11) {
throw new Error(`Invalid month: ${month + 1}. Expected 1-12`)
}
if (day < 1 || day > 31) {
throw new Error(`Invalid day: ${day}. Expected 1-31`)
}
return new Date(year, month, day)
}
/**
* Validate parsed path structure
*/
validate(parsed: ParsedSemanticPath): boolean {
if (!parsed || typeof parsed !== 'object') {
return false
}
if (!parsed.dimension) {
return false
}
if (parsed.value === undefined || parsed.value === null) {
return false
}
// Dimension-specific validation
switch (parsed.dimension) {
case 'time':
return parsed.value instanceof Date && !isNaN(parsed.value.getTime())
case 'relationship':
const relValue = parsed.value as RelationshipValue
return typeof relValue.targetPath === 'string' && relValue.targetPath.length > 0
case 'similar':
const simValue = parsed.value as SimilarityValue
return typeof simValue.targetPath === 'string' && simValue.targetPath.length > 0
default:
return typeof parsed.value === 'string' && parsed.value.length > 0
}
}
/**
* Parse relationship paths: /related-to/<path>/depth-N/types-X,Y/<subpath>
*/
private parseRelationshipPath(path: string): ParsedSemanticPath {
// Remove /related-to/ prefix
const withoutPrefix = path.substring('/related-to/'.length)
// Split into segments
const segments = withoutPrefix.split('/')
let targetPath = ''
let depth: number | undefined
let types: string[] | undefined
let subpath: string | undefined
let i = 0
// Collect path segments until we hit depth-, types-, or end
while (i < segments.length) {
const segment = segments[i]
if (segment.startsWith('depth-')) {
depth = parseInt(segment.substring('depth-'.length), 10)
i++
continue
}
if (segment.startsWith('types-')) {
types = segment.substring('types-'.length).split(',')
i++
continue
}
// If we've already collected the target path and found depth/types,
// rest is subpath
if (targetPath && (depth !== undefined || types !== undefined)) {
subpath = segments.slice(i).join('/')
break
}
// Add to target path
if (targetPath) {
targetPath += '/' + segment
} else {
targetPath = segment
}
i++
}
const value: RelationshipValue = {
targetPath,
depth,
relationshipTypes: types
}
return {
dimension: 'relationship',
value,
subpath
}
}
/**
* Parse similarity paths: /similar-to/<path>/threshold-N/<subpath>
*/
private parseSimilarityPath(path: string): ParsedSemanticPath {
// Remove /similar-to/ prefix
const withoutPrefix = path.substring('/similar-to/'.length)
// Split into segments
const segments = withoutPrefix.split('/')
let targetPath = ''
let threshold: number | undefined
let subpath: string | undefined
let i = 0
// Collect path segments until we hit threshold- or end
while (i < segments.length) {
const segment = segments[i]
if (segment.startsWith('threshold-')) {
threshold = parseFloat(segment.substring('threshold-'.length))
i++
// Rest is subpath
if (i < segments.length) {
subpath = segments.slice(i).join('/')
}
break
}
// Add to target path
if (targetPath) {
targetPath += '/' + segment
} else {
targetPath = segment
}
i++
}
const value: SimilarityValue = {
targetPath,
threshold
}
return {
dimension: 'similar',
value,
subpath
}
}
}

View file

@ -0,0 +1,326 @@
/**
* Semantic Path Resolver
*
* Unified path resolver that handles BOTH:
* - Traditional hierarchical paths (/src/auth/login.ts)
* - Semantic projection paths (/by-concept/authentication/...)
*
* Uses EXISTING infrastructure:
* - PathResolver for traditional paths
* - ProjectionRegistry for semantic dimensions
* - SemanticPathParser for path type detection
*/
import { Brainy } from '../../brainy.js'
import { VirtualFileSystem } from '../VirtualFileSystem.js'
import { PathResolver } from '../PathResolver.js'
import { VFSEntity, VFSError, VFSErrorCode } from '../types.js'
import { SemanticPathParser, ParsedSemanticPath } from './SemanticPathParser.js'
import { ProjectionRegistry } from './ProjectionRegistry.js'
import { UnifiedCache } from '../../utils/unifiedCache.js'
/**
* Semantic Path Resolver
* Handles both traditional and semantic paths transparently
*
* Uses Brainy's UnifiedCache for optimal memory management and performance
*/
export class SemanticPathResolver {
private brain: Brainy
private vfs: VirtualFileSystem
private pathResolver: PathResolver
private parser: SemanticPathParser
private registry: ProjectionRegistry
private cache: UnifiedCache
constructor(
brain: Brainy,
vfs: VirtualFileSystem,
rootEntityId: string,
registry: ProjectionRegistry
) {
this.brain = brain
this.vfs = vfs
this.registry = registry
this.parser = new SemanticPathParser()
// Use Brainy's UnifiedCache for semantic path caching
// Zero-config: Uses 2GB default from UnifiedCache
this.cache = new UnifiedCache({
enableRequestCoalescing: true,
enableFairnessCheck: true
})
// Create traditional path resolver (uses its own optimized cache with defaults)
this.pathResolver = new PathResolver(brain, rootEntityId)
}
/**
* Resolve a path to entity ID(s)
* Handles BOTH traditional and semantic paths
*
* For traditional paths: Returns single entity ID
* For semantic paths: Returns first matching entity ID
*
* Uses UnifiedCache with request coalescing to prevent stampede
*
* @param path - Path to resolve (traditional or semantic)
* @param options - Resolution options
* @returns Entity ID
*/
async resolve(path: string, options?: {
followSymlinks?: boolean
cache?: boolean
}): Promise<string> {
// Parse the path to determine dimension
const parsed = this.parser.parse(path)
// Handle based on path dimension
if (parsed.dimension === 'traditional') {
// Use existing PathResolver for traditional paths
return await this.pathResolver.resolve(path, options)
}
// Semantic path - use UnifiedCache with request coalescing
const cacheKey = `semantic:${path}`
if (options?.cache === false) {
// Skip cache if requested
const entityIds = await this.resolveSemanticPathInternal(parsed)
if (entityIds.length === 0) {
throw new VFSError(
VFSErrorCode.ENOENT,
`No entities found for semantic path: ${path}`,
path,
'resolve'
)
}
return entityIds[0]
}
// Use UnifiedCache - automatically handles stampede prevention
const entityIds = await this.cache.get(cacheKey, async () => {
return await this.resolveSemanticPathInternal(parsed)
})
if (!entityIds || entityIds.length === 0) {
throw new VFSError(
VFSErrorCode.ENOENT,
`No entities found for semantic path: ${path}`,
path,
'resolve'
)
}
return entityIds[0]
}
/**
* Resolve semantic path to multiple entity IDs
* This is the polymorphic resolution that returns ALL matches
*
* Uses UnifiedCache for performance
*
* @param path - Semantic path
* @param options - Resolution options
* @returns Array of entity IDs
*/
async resolveAll(path: string, options?: {
cache?: boolean
limit?: number
}): Promise<string[]> {
const parsed = this.parser.parse(path)
if (parsed.dimension === 'traditional') {
// Traditional paths resolve to single entity
const id = await this.pathResolver.resolve(path, options)
return [id]
}
// Use cache if enabled
const cacheKey = `semantic:${path}`
if (options?.cache === false) {
return await this.resolveSemanticPathInternal(parsed, options?.limit)
}
// UnifiedCache with automatic stampede prevention
return await this.cache.get(cacheKey, async () => {
return await this.resolveSemanticPathInternal(parsed, options?.limit)
})
}
/**
* Internal semantic path resolution (called by cache)
* Estimates cost and size for UnifiedCache optimization
*/
private async resolveSemanticPathInternal(
parsed: ParsedSemanticPath,
limit?: number
): Promise<string[]> {
// Resolve based on dimension
let entityIds: string[] = []
switch (parsed.dimension) {
case 'concept':
entityIds = await this.registry.resolve('concept', parsed.value, this.brain, this.vfs)
break
case 'author':
entityIds = await this.registry.resolve('author', parsed.value, this.brain, this.vfs)
break
case 'time':
entityIds = await this.registry.resolve('time', parsed.value, this.brain, this.vfs)
break
case 'relationship':
entityIds = await this.registry.resolve('relationship', parsed.value, this.brain, this.vfs)
break
case 'similar':
entityIds = await this.registry.resolve('similar', parsed.value, this.brain, this.vfs)
break
case 'tag':
// Tags use metadata filtering (concept-like)
entityIds = await this.registry.resolve('tag', parsed.value, this.brain, this.vfs)
break
case 'traditional':
// Shouldn't reach here, but handle it gracefully
return []
default:
throw new VFSError(
VFSErrorCode.ENOTDIR, // Use existing error code
`Unsupported semantic path dimension: ${parsed.dimension}`,
'',
'resolve'
)
}
// Apply subpath filter if specified
if (parsed.subpath) {
entityIds = await this.filterBySubpath(entityIds, parsed.subpath)
}
// Apply limit
if (limit && limit > 0) {
entityIds = entityIds.slice(0, limit)
}
// Result will be cached by UnifiedCache.get() automatically
return entityIds
}
/**
* Filter entity IDs by subpath (filename or partial path)
*/
private async filterBySubpath(entityIds: string[], subpath: string): Promise<string[]> {
const filtered: string[] = []
for (const id of entityIds) {
const entity = await this.brain.get(id)
if (!entity) continue
const name = entity.metadata?.name
const path = entity.metadata?.path
// Check if name or path matches subpath
if (name === subpath || path?.endsWith(subpath)) {
filtered.push(id)
}
}
return filtered
}
/**
* Get children of a directory
* Delegates to PathResolver for traditional directories
* For semantic paths, returns entities in that dimension
*/
async getChildren(dirIdOrPath: string): Promise<VFSEntity[]> {
// If it looks like a path, parse it
if (dirIdOrPath.startsWith('/')) {
const parsed = this.parser.parse(dirIdOrPath)
if (parsed.dimension !== 'traditional') {
// For semantic paths, list entities in that dimension
return await this.listSemanticDimension(parsed)
}
}
// Traditional directory - use PathResolver
return await this.pathResolver.getChildren(dirIdOrPath)
}
/**
* List entities in a semantic dimension
*/
private async listSemanticDimension(parsed: ParsedSemanticPath): Promise<VFSEntity[]> {
switch (parsed.dimension) {
case 'concept':
return await this.registry.list('concept', this.brain, this.vfs)
case 'author':
return await this.registry.list('author', this.brain, this.vfs)
case 'time':
return await this.registry.list('time', this.brain, this.vfs)
case 'tag':
return await this.registry.list('tag', this.brain, this.vfs)
default:
return []
}
}
/**
* Create a path mapping (cache a path resolution)
* Only applies to traditional paths
*/
async createPath(path: string, entityId: string): Promise<void> {
const parsed = this.parser.parse(path)
if (parsed.dimension === 'traditional') {
await this.pathResolver.createPath(path, entityId)
}
// Semantic paths are not cached via createPath
}
/**
* Invalidate path cache
*/
invalidatePath(path: string, recursive = false): void {
const parsed = this.parser.parse(path)
if (parsed.dimension === 'traditional') {
this.pathResolver.invalidatePath(path, recursive)
} else {
// Invalidate semantic cache via UnifiedCache
const cacheKey = `semantic:${path}`
this.cache.delete(cacheKey)
}
}
/**
* Clear all semantic caches
* Uses UnifiedCache's clear method
*/
invalidateSemanticCache(): void {
this.cache.clear()
}
/**
* Cleanup resources
*/
cleanup(): void {
this.pathResolver.cleanup()
this.cache.clear()
}
}

28
src/vfs/semantic/index.ts Normal file
View file

@ -0,0 +1,28 @@
/**
* Semantic VFS - Index
*
* Central export point for all semantic VFS components
*/
// Core components
export { SemanticPathParser } from './SemanticPathParser.js'
export type {
SemanticDimension,
ParsedSemanticPath,
RelationshipValue,
SimilarityValue
} from './SemanticPathParser.js'
export { ProjectionRegistry } from './ProjectionRegistry.js'
export type { ProjectionStrategy } from './ProjectionStrategy.js'
export { BaseProjectionStrategy } from './ProjectionStrategy.js'
export { SemanticPathResolver } from './SemanticPathResolver.js'
// Built-in projections
export { ConceptProjection } from './projections/ConceptProjection.js'
export { AuthorProjection } from './projections/AuthorProjection.js'
export { TemporalProjection } from './projections/TemporalProjection.js'
export { RelationshipProjection } from './projections/RelationshipProjection.js'
export { SimilarityProjection } from './projections/SimilarityProjection.js'
export { TagProjection } from './projections/TagProjection.js'

View file

@ -0,0 +1,83 @@
/**
* Author Projection Strategy
*
* Maps author-based paths to files owned by that author
* Uses EXISTING MetadataIndexManager for O(log n) queries
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { VFSEntity } from '../../types.js'
/**
* Author Projection: /by-author/<authorName>/<subpath>
*
* Uses EXISTING infrastructure:
* - Brainy.find() with metadata filters (REAL)
* - MetadataIndexManager for O(log n) owner queries (REAL)
* - VFSMetadata.owner field (REAL - types.ts line 44)
*/
export class AuthorProjection extends BaseProjectionStrategy {
readonly name = 'author'
/**
* Convert author name to Brainy FindParams
*/
toQuery(authorName: string, subpath?: string): FindParams {
const query: FindParams = {
where: {
vfsType: 'file',
owner: authorName
},
limit: 1000
}
// Filter by filename if subpath specified
if (subpath) {
query.where = {
...query.where,
anyOf: [ // BFO logical operator (not $or)
{ name: subpath },
{ path: { endsWith: subpath } } // BFO operator (not $regex)
]
}
}
return query
}
/**
* Resolve author to entity IDs using REAL Brainy.find()
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, authorName: string): Promise<string[]> {
// Use REAL Brainy metadata filtering
const results = await brain.find({
where: {
vfsType: 'file',
owner: authorName
},
limit: 1000
})
return this.extractIds(results)
}
/**
* List all unique authors
* Uses aggregation over metadata
*/
async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise<VFSEntity[]> {
// Get all files with owner metadata
const results = await brain.find({
where: {
vfsType: 'file',
owner: { $exists: true }
},
limit
})
return results.map(r => r.entity as VFSEntity)
}
}

View file

@ -0,0 +1,97 @@
/**
* Concept Projection Strategy
*
* Maps concept-based paths to files containing those concepts
* Uses EXISTING ConceptSystem and MetadataIndexManager
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { VFSEntity } from '../../types.js'
/**
* Concept Projection: /by-concept/<conceptName>/<subpath>
*
* Uses EXISTING infrastructure:
* - Brainy.find() with metadata filters (REAL - line 580 in brainy.ts)
* - MetadataIndexManager for O(log n) concept queries (REAL)
* - ConceptSystem for concept extraction (REAL - ConceptSystem.ts)
*/
export class ConceptProjection extends BaseProjectionStrategy {
readonly name = 'concept'
/**
* Convert concept name to Brainy FindParams
* Uses EXISTING FindParams.where for metadata filtering
*
* Now uses flattened conceptNames array for O(log n) performance!
*/
toQuery(conceptName: string, subpath?: string): FindParams {
const query: FindParams = {
where: {
vfsType: 'file',
conceptNames: { contains: conceptName } // O(log n) indexed query
},
limit: 1000
}
// If subpath specified, also filter by filename
if (subpath) {
query.where = {
...query.where,
anyOf: [ // BFO logical operator
{ name: subpath },
{ path: { endsWith: subpath } } // BFO operator
]
}
}
return query
}
/**
* Resolve concept to entity IDs using REAL Brainy.find()
* VERIFIED: brain.find() exists at line 580 in brainy.ts
*
* NOW OPTIMIZED: Uses flattened conceptNames for O(log n) indexed queries!
* No more post-filtering - direct index lookup
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, conceptName: string): Promise<string[]> {
// Verify brain.find is a function (safety check)
if (typeof brain.find !== 'function') {
throw new Error('VERIFICATION FAILED: brain.find is not a function')
}
// Direct O(log n) query using flattened conceptNames array
// VFS automatically flattens concepts to conceptNames on write
const results = await brain.find({
where: {
vfsType: 'file',
conceptNames: { contains: conceptName } // Indexed array query
},
limit: 1000
})
return this.extractIds(results)
}
/**
* List all files with concept metadata
* Uses REAL Brainy.find() with metadata filter
*/
async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise<VFSEntity[]> {
const results = await brain.find({
where: {
vfsType: 'file',
conceptNames: { exists: true } // Use flattened field
},
limit
})
// Convert to VFSEntity array
// VERIFIED: Result.entity exists in brainy.types.ts
return results.map(r => r.entity as VFSEntity)
}
}

View file

@ -0,0 +1,136 @@
/**
* Relationship Projection Strategy
*
* Maps relationship-based paths to files connected in the knowledge graph
* Uses EXISTING GraphAdjacencyIndex for O(1) traversal
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { VerbType } from '../../../types/graphTypes.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { RelationshipValue } from '../SemanticPathParser.js'
/**
* Relationship Projection: /related-to/<path>/depth-N/types-X,Y
*
* Uses EXISTING infrastructure:
* - Brainy.getRelations() for graph traversal (REAL - line 803 in brainy.ts)
* - GraphAdjacencyIndex for O(1) neighbor lookups (REAL)
* - VerbType enum for relationship types (REAL - graphTypes.ts)
*/
export class RelationshipProjection extends BaseProjectionStrategy {
readonly name = 'relationship'
/**
* Convert relationship value to Brainy FindParams
* Note: Graph queries don't use FindParams, but we provide this for consistency
*/
toQuery(value: RelationshipValue, subpath?: string): FindParams {
// This is informational - actual resolution uses getRelations()
return {
where: {
vfsType: 'file'
},
connected: {
to: value.targetPath,
depth: value.depth || 1
},
limit: 1000
}
}
/**
* Resolve relationships using REAL Brainy.getRelations()
* Uses GraphAdjacencyIndex for O(1) graph traversal
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, value: RelationshipValue): Promise<string[]> {
// Step 1: Resolve target path to entity ID
const targetId = await this.resolvePathToId(vfs, value.targetPath)
if (!targetId) {
return []
}
// Step 2: Get relationships using REAL Brainy graph
const depth = value.depth || 1
const visited = new Set<string>()
const results: string[] = []
await this.traverseRelationships(
brain,
targetId,
depth,
visited,
results,
value.relationshipTypes
)
// Filter to only files
return await this.filterFiles(brain, results)
}
/**
* Recursive graph traversal using REAL Brainy.getRelations()
*/
private async traverseRelationships(
brain: Brainy,
entityId: string,
remainingDepth: number,
visited: Set<string>,
results: string[],
types?: string[]
): Promise<void> {
if (remainingDepth <= 0 || visited.has(entityId)) {
return
}
visited.add(entityId)
// Get outgoing relationships (REAL method - line 803 in brainy.ts)
const relations = await brain.getRelations({
from: entityId,
limit: 100
})
for (const relation of relations) {
// Filter by relationship type if specified
if (types && types.length > 0) {
const relationshipName = relation.type?.toLowerCase()
if (!types.some(t => t.toLowerCase() === relationshipName)) {
continue
}
}
// Add to results
if (!results.includes(relation.to)) {
results.push(relation.to)
}
// Recurse if depth remaining
if (remainingDepth > 1) {
await this.traverseRelationships(
brain,
relation.to,
remainingDepth - 1,
visited,
results,
types
)
}
}
}
/**
* Resolve path to entity ID
* Helper to convert traditional path to entity ID
*/
private async resolvePathToId(vfs: VirtualFileSystem, path: string): Promise<string | null> {
try {
// Use REAL VFS public method
return await vfs.resolvePath(path)
} catch {
return null
}
}
}

View file

@ -0,0 +1,84 @@
/**
* Similarity Projection Strategy
*
* Maps similarity-based paths to files with similar content
* Uses EXISTING HNSW Index for O(log n) vector similarity
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { SimilarityValue } from '../SemanticPathParser.js'
/**
* Similarity Projection: /similar-to/<path>/threshold-N
*
* Uses EXISTING infrastructure:
* - Brainy.similar() for vector similarity (REAL - line 680 in brainy.ts)
* - HNSW Index for O(log n) nearest neighbor search (REAL)
* - Cosine similarity for scoring (REAL)
*/
export class SimilarityProjection extends BaseProjectionStrategy {
readonly name = 'similar'
/**
* Convert similarity value to Brainy FindParams
* Note: Similarity uses brain.similar(), not find(), but we provide this for consistency
*/
toQuery(value: SimilarityValue, subpath?: string): FindParams {
// This is informational - actual resolution uses brain.similar()
return {
where: {
vfsType: 'file'
},
near: {
id: value.targetPath,
threshold: value.threshold || 0.7
},
limit: 50
}
}
/**
* Resolve similarity using REAL Brainy.similar()
* Uses HNSW Index for O(log n) vector search
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, value: SimilarityValue): Promise<string[]> {
// Step 1: Resolve target path to entity ID
const targetId = await this.resolvePathToId(vfs, value.targetPath)
if (!targetId) {
return []
}
// Step 2: Get target entity to use its vector
const targetEntity = await brain.get(targetId)
if (!targetEntity) {
return []
}
// Step 3: Find similar entities using REAL HNSW search
// VERIFIED: brain.similar() exists at line 680 in brainy.ts
const results = await brain.similar({
to: targetEntity,
threshold: value.threshold || 0.7,
limit: 50,
where: { vfsType: 'file' } // Only files
})
// Extract IDs
return this.extractIds(results)
}
/**
* Resolve path to entity ID
*/
private async resolvePathToId(vfs: VirtualFileSystem, path: string): Promise<string | null> {
try {
// Use REAL VFS public method
return await vfs.resolvePath(path)
} catch {
return null
}
}
}

View file

@ -0,0 +1,82 @@
/**
* Tag Projection Strategy
*
* Maps tag-based paths to files with those tags
* Uses EXISTING MetadataIndexManager for O(log n) queries
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { VFSEntity } from '../../types.js'
/**
* Tag Projection: /by-tag/<tagName>/<subpath>
*
* Uses EXISTING infrastructure:
* - Brainy.find() with metadata filters (REAL)
* - MetadataIndexManager for O(log n) tag queries (REAL)
* - VFSMetadata.tags field (REAL - types.ts line 66)
*/
export class TagProjection extends BaseProjectionStrategy {
readonly name = 'tag'
/**
* Convert tag name to Brainy FindParams
*/
toQuery(tagName: string, subpath?: string): FindParams {
const query: FindParams = {
where: {
vfsType: 'file',
tags: { contains: tagName } // BFO operator for array contains
},
limit: 1000
}
// Filter by filename if subpath specified
if (subpath) {
query.where = {
...query.where,
anyOf: [ // BFO logical operator
{ name: subpath },
{ path: { endsWith: subpath } } // BFO operator
]
}
}
return query
}
/**
* Resolve tag to entity IDs using REAL Brainy.find()
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, tagName: string): Promise<string[]> {
// Use REAL Brainy metadata filtering
const results = await brain.find({
where: {
vfsType: 'file',
tags: { contains: tagName } // BFO operator
},
limit: 1000
})
return this.extractIds(results)
}
/**
* List all files with tags
*/
async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise<VFSEntity[]> {
// Get all files that have tags
const results = await brain.find({
where: {
vfsType: 'file',
tags: { exists: true } // BFO operator
},
limit
})
return results.map(r => r.entity as VFSEntity)
}
}

View file

@ -0,0 +1,103 @@
/**
* Temporal Projection Strategy
*
* Maps time-based paths to files modified at that time
* Uses EXISTING MetadataIndexManager with range queries
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { VFSEntity } from '../../types.js'
/**
* Temporal Projection: /as-of/<YYYY-MM-DD>/<subpath>
*
* Uses EXISTING infrastructure:
* - Brainy.find() with range queries (REAL)
* - MetadataIndexManager.$gte/$lte operators (REAL)
* - VFSMetadata.modified field (REAL - types.ts line 49)
*/
export class TemporalProjection extends BaseProjectionStrategy {
readonly name = 'time'
/**
* Convert date to Brainy FindParams with range query
*/
toQuery(date: Date, subpath?: string): FindParams {
// Get start and end of day (24-hour window)
const startOfDay = new Date(date)
startOfDay.setHours(0, 0, 0, 0)
const endOfDay = new Date(date)
endOfDay.setHours(23, 59, 59, 999)
const query: FindParams = {
where: {
vfsType: 'file',
modified: {
greaterEqual: startOfDay.getTime(), // BFO operator
lessEqual: endOfDay.getTime() // BFO operator
}
},
limit: 1000
}
// Filter by filename if subpath specified
if (subpath) {
query.where = {
...query.where,
anyOf: [ // BFO logical operator (not $or)
{ name: subpath },
{ path: { endsWith: subpath } } // BFO operator (not $regex)
]
}
}
return query
}
/**
* Resolve date to entity IDs using REAL Brainy.find()
* Uses MetadataIndexManager range queries for O(log n) performance
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, date: Date): Promise<string[]> {
const startOfDay = new Date(date)
startOfDay.setHours(0, 0, 0, 0)
const endOfDay = new Date(date)
endOfDay.setHours(23, 59, 59, 999)
// Use REAL Brainy metadata filtering with range operators
const results = await brain.find({
where: {
vfsType: 'file',
modified: {
greaterEqual: startOfDay.getTime(), // BFO operator
lessEqual: endOfDay.getTime() // BFO operator
}
},
limit: 1000
})
return this.extractIds(results)
}
/**
* List recently modified files
*/
async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise<VFSEntity[]> {
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000)
const results = await brain.find({
where: {
vfsType: 'file',
modified: { greaterEqual: oneDayAgo } // BFO operator
},
limit
})
return results.map(r => r.entity as VFSEntity)
}
}