UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

522 lines (436 loc) 16.7 kB
/** * Semantic Pattern Search - Story 12.7 * * Find patterns by semantic similarity, not just exact string matches. * Merges exact, semantic, and subsequence matches with confidence scoring. * * @module semantic-search * @version 1.0.0 */ 'use strict'; // ═══════════════════════════════════════════════════════════════════════════════════ // CONFIGURATION // ═══════════════════════════════════════════════════════════════════════════════════ const DEFAULT_CONFIG = { // Match weights exactMatchWeight: 1.0, semanticMatchWeight: 0.7, subsequenceMatchWeight: 0.5, fuzzyMatchWeight: 0.3, // Thresholds minSimilarityScore: 0.3, maxResults: 10, // Performance maxQueryLength: 100, cacheEnabled: true, cacheTTL: 60000, // 1 minute // Synonym groups for semantic matching synonymGroups: [ ['create', 'make', 'generate', 'add', 'new'], ['delete', 'remove', 'destroy', 'drop'], ['update', 'modify', 'change', 'edit', 'alter'], ['get', 'fetch', 'retrieve', 'load', 'read'], ['list', 'show', 'display', 'view'], ['test', 'verify', 'check', 'validate'], ['build', 'compile', 'bundle', 'package'], ['deploy', 'publish', 'release', 'ship'], ['fix', 'repair', 'resolve', 'patch'], ['review', 'inspect', 'audit', 'examine'], ['story', 'task', 'ticket', 'issue'], ['dev', 'develop', 'development', 'implement'], ['qa', 'quality', 'testing'], ['start', 'begin', 'init', 'initialize'], ['end', 'finish', 'complete', 'done'], ], }; // ═══════════════════════════════════════════════════════════════════════════════════ // SEMANTIC SEARCH ENGINE // ═══════════════════════════════════════════════════════════════════════════════════ class SemanticSearch { /** * Create a new SemanticSearch instance * * @param {Object} options - Configuration options * @param {Object} [options.config] - Config overrides */ constructor(options = {}) { this.config = { ...DEFAULT_CONFIG, ...options.config }; this._synonymIndex = this._buildSynonymIndex(); this._cache = new Map(); this._lastCacheClean = Date.now(); } // ───────────────────────────────────────────────────────────────────────────────── // SEARCH // ───────────────────────────────────────────────────────────────────────────────── /** * Search patterns by semantic similarity * * @param {string} query - Search query * @param {Object[]} patterns - Patterns to search * @param {Object} options - Search options * @returns {Object[]} Matched patterns with scores */ search(query, patterns, options = {}) { if (!query || !patterns || patterns.length === 0) { return []; } const startTime = Date.now(); // Normalize query const normalizedQuery = this._normalize(query); if (normalizedQuery.length === 0) { return []; } // Check cache const cacheKey = this._getCacheKey(normalizedQuery, patterns.length); if (this.config.cacheEnabled) { const cached = this._getFromCache(cacheKey); if (cached) { return cached; } } // Score all patterns const scored = patterns .map((pattern) => { const score = this._scorePattern(normalizedQuery, pattern); return { pattern, score }; }) .filter((item) => item.score >= this.config.minSimilarityScore) .sort((a, b) => b.score - a.score) .slice(0, options.maxResults || this.config.maxResults); // Add metadata const results = scored.map((item) => ({ ...item.pattern, _searchScore: item.score, _searchMethod: this._determineMatchMethod(item.score), })); // Cache results if (this.config.cacheEnabled) { this._setCache(cacheKey, results); } // Performance check const elapsed = Date.now() - startTime; if (elapsed > 100) { console.warn(`SemanticSearch: Query took ${elapsed}ms (target <100ms)`); } return results; } /** * Find similar patterns to a given pattern * * @param {Object} pattern - Reference pattern * @param {Object[]} patterns - Patterns to compare * @param {Object} options - Search options * @returns {Object[]} Similar patterns */ findSimilar(pattern, patterns, options = {}) { if (!pattern || !patterns) { return []; } // Build query from pattern const queryParts = []; if (pattern.name) queryParts.push(pattern.name); if (pattern.workflow) queryParts.push(pattern.workflow); if (pattern.sequence) queryParts.push(...pattern.sequence); if (pattern.commands) queryParts.push(...pattern.commands); const query = queryParts.join(' '); // Exclude the reference pattern const candidatePatterns = patterns.filter((p) => p.id !== pattern.id); return this.search(query, candidatePatterns, options); } // ───────────────────────────────────────────────────────────────────────────────── // SCORING // ───────────────────────────────────────────────────────────────────────────────── /** * Score a pattern against a query * @private */ _scorePattern(query, pattern) { const scores = []; // Get searchable text from pattern const patternText = this._getPatternText(pattern); const normalizedPattern = this._normalize(patternText); // 1. Exact match const exactScore = this._exactMatch(query, normalizedPattern); if (exactScore > 0) { scores.push(exactScore * this.config.exactMatchWeight); } // 2. Semantic match (synonym-based) const semanticScore = this._semanticMatch(query, normalizedPattern); if (semanticScore > 0) { scores.push(semanticScore * this.config.semanticMatchWeight); } // 3. Subsequence match const subsequenceScore = this._subsequenceMatch(query, normalizedPattern); if (subsequenceScore > 0) { scores.push(subsequenceScore * this.config.subsequenceMatchWeight); } // 4. Fuzzy match (Levenshtein-based) const fuzzyScore = this._fuzzyMatch(query, normalizedPattern); if (fuzzyScore > 0) { scores.push(fuzzyScore * this.config.fuzzyMatchWeight); } // Return best score (not sum, to avoid over-counting) return scores.length > 0 ? Math.max(...scores) : 0; } /** * Exact substring match * @private */ _exactMatch(query, text) { if (text.includes(query)) { return 1.0; } // Check word-level exact match const queryWords = query.split(/\s+/); const textWords = text.split(/\s+/); const matchedWords = queryWords.filter((qw) => textWords.includes(qw)); return matchedWords.length / queryWords.length; } /** * Semantic match using synonyms * @private */ _semanticMatch(query, text) { const queryWords = query.split(/\s+/); const textWords = text.split(/\s+/); let matchCount = 0; for (const qw of queryWords) { // Direct match if (textWords.includes(qw)) { matchCount++; continue; } // Synonym match const synonyms = this._getSynonyms(qw); if (synonyms.some((syn) => textWords.includes(syn))) { matchCount += 0.8; // Slightly lower weight for synonym match } } return matchCount / queryWords.length; } /** * Subsequence match (words appear in order, not necessarily adjacent) * @private */ _subsequenceMatch(query, text) { const queryWords = query.split(/\s+/); const textWords = text.split(/\s+/); let queryIndex = 0; let matchCount = 0; for (const tw of textWords) { if (queryIndex < queryWords.length && tw === queryWords[queryIndex]) { matchCount++; queryIndex++; } } return matchCount / queryWords.length; } /** * Fuzzy match using Levenshtein distance * @private */ _fuzzyMatch(query, text) { const queryWords = query.split(/\s+/); const textWords = text.split(/\s+/); let totalSimilarity = 0; for (const qw of queryWords) { let bestSimilarity = 0; for (const tw of textWords) { const distance = this._levenshteinDistance(qw, tw); const maxLen = Math.max(qw.length, tw.length); const similarity = 1 - distance / maxLen; if (similarity > bestSimilarity) { bestSimilarity = similarity; } } totalSimilarity += bestSimilarity; } return totalSimilarity / queryWords.length; } /** * Calculate Levenshtein distance * @private */ _levenshteinDistance(a, b) { if (a.length === 0) return b.length; if (b.length === 0) return a.length; const matrix = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1, ); } } } return matrix[b.length][a.length]; } // ───────────────────────────────────────────────────────────────────────────────── // HELPERS // ───────────────────────────────────────────────────────────────────────────────── /** * Normalize text for comparison * @private */ _normalize(text) { if (!text) return ''; return text .toLowerCase() .replace(/[^a-z0-9\s]/g, ' ') .replace(/\s+/g, ' ') .trim() .slice(0, this.config.maxQueryLength); } /** * Get searchable text from pattern * @private */ _getPatternText(pattern) { const parts = []; if (pattern.name) parts.push(pattern.name); if (pattern.id) parts.push(pattern.id); if (pattern.workflow) parts.push(pattern.workflow); if (pattern.description) parts.push(pattern.description); if (pattern.sequence) parts.push(pattern.sequence.join(' ')); if (pattern.commands) parts.push(pattern.commands.join(' ')); if (pattern.agents) parts.push(pattern.agents.join(' ')); if (pattern.keywords) parts.push(pattern.keywords.join(' ')); return parts.join(' '); } /** * Build synonym index for fast lookup * @private */ _buildSynonymIndex() { const index = new Map(); for (const group of this.config.synonymGroups) { for (const word of group) { const synonyms = group.filter((w) => w !== word); index.set(word, synonyms); } } return index; } /** * Get synonyms for a word * @private */ _getSynonyms(word) { return this._synonymIndex.get(word) || []; } /** * Determine match method from score * @private */ _determineMatchMethod(score) { if (score >= 0.9) return 'exact'; if (score >= 0.6) return 'semantic'; if (score >= 0.4) return 'subsequence'; return 'fuzzy'; } // ───────────────────────────────────────────────────────────────────────────────── // CACHE // ───────────────────────────────────────────────────────────────────────────────── /** * Get cache key * @private */ _getCacheKey(query, patternCount) { return `${query}:${patternCount}`; } /** * Get from cache * @private */ _getFromCache(key) { this._cleanCache(); const entry = this._cache.get(key); if (entry && Date.now() - entry.timestamp < this.config.cacheTTL) { return entry.results; } return null; } /** * Set cache entry * @private */ _setCache(key, results) { this._cache.set(key, { results, timestamp: Date.now(), }); } /** * Clean expired cache entries * @private */ _cleanCache() { const now = Date.now(); // Only clean every 30 seconds if (now - this._lastCacheClean < 30000) { return; } this._lastCacheClean = now; for (const [key, entry] of this._cache.entries()) { if (now - entry.timestamp >= this.config.cacheTTL) { this._cache.delete(key); } } } /** * Clear cache */ clearCache() { this._cache.clear(); } // ───────────────────────────────────────────────────────────────────────────────── // STATISTICS // ───────────────────────────────────────────────────────────────────────────────── /** * Get search statistics * * @returns {Object} Statistics */ getStatistics() { return { cacheSize: this._cache.size, synonymGroups: this.config.synonymGroups.length, synonymCount: this._synonymIndex.size, config: { minSimilarityScore: this.config.minSimilarityScore, maxResults: this.config.maxResults, }, }; } } // ═══════════════════════════════════════════════════════════════════════════════════ // FACTORY // ═══════════════════════════════════════════════════════════════════════════════════ /** * Create a new SemanticSearch instance * * @param {Object} options - Options * @returns {SemanticSearch} New instance */ function createSemanticSearch(options = {}) { return new SemanticSearch(options); } // ═══════════════════════════════════════════════════════════════════════════════════ // EXPORTS // ═══════════════════════════════════════════════════════════════════════════════════ module.exports = { SemanticSearch, createSemanticSearch, DEFAULT_CONFIG, };