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
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in a new issue