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:
parent
27cc699555
commit
dd50d89ad6
41 changed files with 3807 additions and 7391 deletions
147
src/vfs/semantic/ProjectionRegistry.ts
Normal file
147
src/vfs/semantic/ProjectionRegistry.ts
Normal 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())
|
||||
}
|
||||
}
|
||||
93
src/vfs/semantic/ProjectionStrategy.ts
Normal file
93
src/vfs/semantic/ProjectionStrategy.ts
Normal 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
|
||||
}
|
||||
}
|
||||
363
src/vfs/semantic/SemanticPathParser.ts
Normal file
363
src/vfs/semantic/SemanticPathParser.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
326
src/vfs/semantic/SemanticPathResolver.ts
Normal file
326
src/vfs/semantic/SemanticPathResolver.ts
Normal 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
28
src/vfs/semantic/index.ts
Normal 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'
|
||||
83
src/vfs/semantic/projections/AuthorProjection.ts
Normal file
83
src/vfs/semantic/projections/AuthorProjection.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
97
src/vfs/semantic/projections/ConceptProjection.ts
Normal file
97
src/vfs/semantic/projections/ConceptProjection.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
136
src/vfs/semantic/projections/RelationshipProjection.ts
Normal file
136
src/vfs/semantic/projections/RelationshipProjection.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/vfs/semantic/projections/SimilarityProjection.ts
Normal file
84
src/vfs/semantic/projections/SimilarityProjection.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/vfs/semantic/projections/TagProjection.ts
Normal file
82
src/vfs/semantic/projections/TagProjection.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
103
src/vfs/semantic/projections/TemporalProjection.ts
Normal file
103
src/vfs/semantic/projections/TemporalProjection.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue