UNPKG

aios-core

Version:

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

652 lines (562 loc) 21.4 kB
'use strict'; /** * IncrementalDecisionEngine — IDS Story IDS-2 * * Analyzes developer/agent intent and recommends REUSE, ADAPT, or CREATE * based on existing artifacts in the Entity Registry. * * Algorithm: TF-IDF keyword overlap (60%) + purpose similarity (40%) * Decision: REUSE (>=90%) | ADAPT (60-89% + constraints) | CREATE (<60%) * * Source: ADR-IDS-001 — Incremental Development System */ const STOP_WORDS = new Set([ 'the', 'a', 'an', 'is', 'are', 'for', 'to', 'of', 'in', 'on', 'and', 'or', 'but', 'not', 'with', 'that', 'this', 'it', 'be', 'as', 'at', 'by', 'from', 'has', 'have', 'had', 'was', 'were', 'will', 'would', 'can', 'could', 'should', 'do', 'does', 'did', 'i', 'we', 'you', 'my', 'our', 'your', 'its', 'their', ]); const MIN_KEYWORD_LENGTH = 3; const MAX_KEYWORDS_PER_ENTITY = 15; const KEYWORD_OVERLAP_WEIGHT = 0.6; const PURPOSE_SIMILARITY_WEIGHT = 0.4; const THRESHOLD_MINIMUM = 0.4; const MAX_RESULTS = 20; const CACHE_TTL_MS = 300_000; // 300 seconds const ADAPT_IMPACT_THRESHOLD = 0.30; // Calibrate after 90 days (ADR-IDS-001 Roundtable #2) class IncrementalDecisionEngine { /** * @param {import('./registry-loader').RegistryLoader} registryLoader */ constructor(registryLoader) { if (!registryLoader) { throw new Error('[IDS] IncrementalDecisionEngine requires a RegistryLoader instance'); } this._loader = registryLoader; this._analysisCache = new Map(); this._analysisCacheTimestamps = new Map(); this._idfCache = null; this._idfCacheTimestamp = 0; } // ================================================================ // Task 1: Main API — analyze(intent, context) // ================================================================ /** * Analyze intent and return ranked recommendations with decisions. * @param {string} intent — Natural language description of what is needed * @param {object} [context={}] — Optional context (category, type filters) * @returns {object} Analysis result with recommendations, summary, rationale */ analyze(intent, context = {}) { if (!intent || typeof intent !== 'string' || !intent.trim()) { return { intent, recommendations: [], summary: { totalEntities: 0, matchesFound: 0, decision: 'CREATE', confidence: 'low' }, rationale: 'Empty or invalid intent provided.', warnings: ['Empty or invalid intent provided'], }; } const cacheKey = `${intent.trim().toLowerCase()}|${JSON.stringify(context)}`; const cached = this._getFromCache(cacheKey); if (cached) return cached; try { this._loader._ensureLoaded(); } catch (err) { throw new Error(`[IDS] Failed to load registry: ${err.message}`); } let allEntities; try { allEntities = this._loader._getAllEntities(); } catch (err) { throw new Error(`[IDS] Failed to retrieve entities: ${err.message}`); } const totalEntities = allEntities.length; // Edge case: empty registry if (totalEntities === 0) { const result = { intent, recommendations: [], summary: { totalEntities: 0, matchesFound: 0, decision: 'CREATE', confidence: 'low' }, rationale: 'Registry is empty — no existing artifacts to evaluate.', warnings: ['Registry is empty — no existing artifacts to evaluate'], justification: this._buildCreateJustification(intent, [], allEntities), }; this._setCache(cacheKey, result); return result; } const intentKeywords = this._extractKeywords(intent); const intentPurpose = intent.trim().toLowerCase(); // Filter by context if provided let candidates = allEntities; if (context.type) { candidates = candidates.filter( (e) => e.type && e.type.toLowerCase() === context.type.toLowerCase(), ); } if (context.category) { candidates = candidates.filter( (e) => e.category && e.category.toLowerCase() === context.category.toLowerCase(), ); } // Score all candidate entities const evaluations = []; for (const entity of candidates) { const keywordScore = this._calculateKeywordOverlap(intentKeywords, entity); const purposeScore = this._calculatePurposeSimilarity(intentPurpose, entity); const relevanceScore = keywordScore * KEYWORD_OVERLAP_WEIGHT + purposeScore * PURPOSE_SIMILARITY_WEIGHT; if (relevanceScore >= THRESHOLD_MINIMUM) { evaluations.push({ entity, keywordScore, purposeScore, relevanceScore, canAdapt: entity.adaptability || { score: 0.5, constraints: [], extensionPoints: [] }, }); } } // Sort by relevance descending evaluations.sort((a, b) => b.relevanceScore - a.relevanceScore); const topEvaluations = evaluations.slice(0, MAX_RESULTS); // Build recommendations with decision + impact + rationale (lazy impact) const recommendations = topEvaluations.map((evaluation) => { const adaptationImpact = this._calculateImpact(evaluation.entity, totalEntities); const decision = this._applyDecisionMatrix({ ...evaluation, adaptationImpact }); const rationale = this._generateEntityRationale(evaluation, decision, adaptationImpact); const rec = { entityId: evaluation.entity.id, entityPath: evaluation.entity.path, entityType: evaluation.entity.type, entityPurpose: evaluation.entity.purpose, relevanceScore: this._round(evaluation.relevanceScore), keywordScore: this._round(evaluation.keywordScore), purposeScore: this._round(evaluation.purposeScore), decision: decision.action, confidence: decision.confidence, rationale, }; if (decision.action === 'ADAPT') { rec.adaptationImpact = adaptationImpact; } return rec; }); // Overall summary const topDecision = recommendations.length > 0 ? recommendations[0].decision : 'CREATE'; const topConfidence = recommendations.length > 0 ? recommendations[0].confidence : 'low'; const result = { intent, recommendations, summary: { totalEntities, matchesFound: evaluations.length, decision: topDecision, confidence: topConfidence, }, rationale: this._generateOverallRationale(recommendations, topDecision, evaluations), }; // Sparse registry warning if (totalEntities > 0 && totalEntities < 10) { result.warnings = result.warnings || []; result.warnings.push('Registry sparse — results may be incomplete'); } // CREATE justification (ADR-IDS-001 Roundtable #4) if (topDecision === 'CREATE') { result.justification = this._buildCreateJustification(intent, evaluations, allEntities); } this._setCache(cacheKey, result); return result; } // ================================================================ // Task 2: Semantic Matching // ================================================================ /** * Extract keywords from text using stop-word filtered tokenization. * @param {string} text * @returns {string[]} */ _extractKeywords(text) { if (!text) return []; return text .toLowerCase() .replace(/[^a-z0-9\s-]/g, ' ') .split(/\s+/) .filter((word) => word.length >= MIN_KEYWORD_LENGTH && !STOP_WORDS.has(word)) .slice(0, MAX_KEYWORDS_PER_ENTITY); } /** * Calculate TF-IDF weighted keyword overlap between intent and entity. * @returns {number} Score 0-1 */ _calculateKeywordOverlap(intentKeywords, entity) { if (!intentKeywords.length) return 0; const entityKeywords = (entity.keywords || []).map((k) => k.toLowerCase()); if (!entityKeywords.length) return 0; const idfScores = this._getIdfScores(); let overlapScore = 0; let maxPossibleScore = 0; for (const intentKw of intentKeywords) { const idf = idfScores.get(intentKw) || 1; maxPossibleScore += idf; // Exact match if (entityKeywords.includes(intentKw)) { overlapScore += idf; continue; } // Partial/prefix match (fuzzy fallback) const partialMatch = entityKeywords.some( (ekw) => ekw.startsWith(intentKw) || intentKw.startsWith(ekw), ); if (partialMatch) { overlapScore += idf * 0.5; } } return maxPossibleScore > 0 ? overlapScore / maxPossibleScore : 0; } /** * Build IDF (Inverse Document Frequency) scores for all keywords in registry. * Cached with TTL for performance. * @returns {Map<string, number>} */ _getIdfScores() { const now = Date.now(); if (this._idfCache && now - this._idfCacheTimestamp < CACHE_TTL_MS) { return this._idfCache; } const allEntities = this._loader._getAllEntities(); const totalDocs = allEntities.length || 1; const keywordDocCount = new Map(); for (const entity of allEntities) { const seen = new Set(); for (const kw of entity.keywords || []) { const lower = kw.toLowerCase(); if (!seen.has(lower)) { seen.add(lower); keywordDocCount.set(lower, (keywordDocCount.get(lower) || 0) + 1); } } } const idfScores = new Map(); for (const [keyword, count] of keywordDocCount) { idfScores.set(keyword, Math.log(totalDocs / count) + 1); } this._idfCache = idfScores; this._idfCacheTimestamp = now; return idfScores; } /** * Calculate purpose similarity using token overlap (Jaccard-like). * @returns {number} Score 0-1 */ _calculatePurposeSimilarity(intentPurpose, entity) { if (!intentPurpose || !entity.purpose) return 0; const intentTokens = this._extractKeywords(intentPurpose); const purposeTokens = this._extractKeywords(entity.purpose); if (!intentTokens.length || !purposeTokens.length) return 0; const intentSet = new Set(intentTokens); const purposeSet = new Set(purposeTokens); let matches = 0; for (const token of intentSet) { if (purposeSet.has(token)) { matches++; continue; } // Fuzzy: prefix match for (const pToken of purposeSet) { if (pToken.startsWith(token) || token.startsWith(pToken)) { matches += 0.5; break; } } } const denominator = Math.min(intentSet.size, purposeSet.size); return denominator > 0 ? Math.min(matches / denominator, 1) : 0; } // ================================================================ // Task 3: Decision Matrix // ================================================================ /** * Apply decision matrix to a scored evaluation. * REUSE (>=90%) | ADAPT (60-89% + adaptability >=0.6 + impact <30%) | CREATE * @returns {{ action: string, confidence: string }} */ _applyDecisionMatrix(evaluation) { const { relevanceScore, canAdapt, adaptationImpact } = evaluation; if (relevanceScore >= 0.9) { return { action: 'REUSE', confidence: 'high' }; } if ( relevanceScore >= 0.6 && canAdapt.score >= 0.6 && adaptationImpact.percentage < ADAPT_IMPACT_THRESHOLD ) { const confidence = relevanceScore >= 0.8 ? 'high' : 'medium'; return { action: 'ADAPT', confidence }; } const confidence = relevanceScore >= 0.6 ? 'medium' : 'low'; return { action: 'CREATE', confidence }; } // ================================================================ // Task 4: Impact Analysis // ================================================================ /** * Calculate adaptation impact by traversing usedBy relationships (BFS). * @param {object} entity * @param {number} totalEntities * @returns {object} Impact report */ _calculateImpact(entity, totalEntities) { const directConsumers = entity.usedBy || []; const visited = new Set(); const queue = [...directConsumers]; const allAffected = new Set(directConsumers); while (queue.length > 0) { const consumerId = queue.shift(); if (visited.has(consumerId)) continue; visited.add(consumerId); const consumer = this._loader._findById(consumerId); if (consumer && consumer.usedBy) { for (const indirect of consumer.usedBy) { if (!allAffected.has(indirect)) { allAffected.add(indirect); queue.push(indirect); } } } } const directCount = directConsumers.length; const indirectCount = allAffected.size - directCount; const percentage = totalEntities > 0 ? allAffected.size / totalEntities : 0; return { directConsumers, directCount, indirectCount, totalAffected: allAffected.size, percentage: this._round(percentage), affectedEntities: [...allAffected], }; } // ================================================================ // Task 5: Rationale Generation // ================================================================ /** * Generate rationale for a single entity recommendation. */ _generateEntityRationale(evaluation, decision, impact) { const { entity, relevanceScore, keywordScore, purposeScore, canAdapt } = evaluation; const parts = []; if (decision.action === 'REUSE') { parts.push(`Strong match (${this._pct(relevanceScore)} relevance).`); parts.push( `Keywords align (${this._pct(keywordScore)}), purpose matches (${this._pct(purposeScore)}).`, ); parts.push(`Recommendation: Use "${entity.id}" directly without modification.`); } else if (decision.action === 'ADAPT') { parts.push(`Good match (${this._pct(relevanceScore)} relevance) with adaptation potential.`); parts.push( `Adaptability: ${canAdapt.score}, impact: ${this._pct(impact.percentage)} of entities affected.`, ); if (canAdapt.extensionPoints && canAdapt.extensionPoints.length > 0) { parts.push(`Adaptation points: ${canAdapt.extensionPoints.join(', ')}.`); } if (canAdapt.constraints && canAdapt.constraints.length > 0) { parts.push(`Constraints: ${canAdapt.constraints.join('; ')}.`); } } else { parts.push(`Insufficient match (${this._pct(relevanceScore)} relevance).`); if (relevanceScore >= 0.6 && canAdapt.score < 0.6) { parts.push(`Relevance adequate but adaptability too low (${canAdapt.score}).`); } else if (relevanceScore >= 0.6 && impact.percentage >= ADAPT_IMPACT_THRESHOLD) { parts.push( `Relevance adequate but adaptation impact too high (${this._pct(impact.percentage)}).`, ); } } return parts.join(' '); } /** * Generate overall rationale summarizing the analysis. */ _generateOverallRationale(recommendations, topDecision, evaluations) { if (recommendations.length === 0) { return 'No matches found above minimum threshold. CREATE is recommended.'; } const reuseCount = recommendations.filter((r) => r.decision === 'REUSE').length; const adaptCount = recommendations.filter((r) => r.decision === 'ADAPT').length; const createCount = recommendations.filter((r) => r.decision === 'CREATE').length; const parts = [`Found ${evaluations.length} match(es) above threshold.`]; if (reuseCount > 0) parts.push(`${reuseCount} can be reused directly.`); if (adaptCount > 0) parts.push(`${adaptCount} can be adapted.`); if (createCount > 0) parts.push(`${createCount} evaluated but insufficient for reuse/adaptation.`); parts.push(`Top recommendation: ${topDecision} "${recommendations[0].entityId}".`); return parts.join(' '); } // ================================================================ // Task 9: CREATE Decision Requirements (ADR-IDS-001 Roundtable #4) // ================================================================ /** * Build CREATE justification with evaluated patterns and rejection reasons. * Required fields: evaluated_patterns, rejection_reasons, new_capability. */ _buildCreateJustification(intent, evaluations, allEntities) { const evaluated = evaluations.slice(0, 5); const evaluatedPatterns = evaluated.map((e) => e.entity.id); const rejectionReasons = {}; for (const evaluation of evaluated) { const { entity, relevanceScore, canAdapt } = evaluation; const impact = this._calculateImpact(entity, allEntities.length); const reasons = []; if (relevanceScore < 0.6) { reasons.push(`Low relevance (${this._pct(relevanceScore)})`); } if (canAdapt.score < 0.6) { reasons.push(`Low adaptability (${canAdapt.score})`); } if (impact.percentage >= ADAPT_IMPACT_THRESHOLD) { reasons.push(`High adaptation impact (${this._pct(impact.percentage)})`); } if (reasons.length === 0) { reasons.push('Does not meet combined ADAPT criteria'); } rejectionReasons[entity.id] = reasons.join('; '); } const reviewDate = new Date(); reviewDate.setDate(reviewDate.getDate() + 30); return { evaluated_patterns: evaluatedPatterns, rejection_reasons: rejectionReasons, new_capability: intent.trim(), review_scheduled: reviewDate.toISOString().split('T')[0], }; } /** * Review CREATE decisions across the registry. * Scans entities with createJustification metadata and returns review report. * (Task 9.4 — 30-day review automation) */ reviewCreateDecisions() { try { this._loader._ensureLoaded(); } catch (err) { throw new Error(`[IDS] Failed to load registry: ${err.message}`); } let allEntities; try { allEntities = this._loader._getAllEntities(); } catch (err) { throw new Error(`[IDS] Failed to retrieve entities: ${err.message}`); } const now = new Date(); const report = { pendingReview: [], promotionCandidates: [], monitoring: [], deprecationReview: [], totalReviewed: 0, }; for (const entity of allEntities) { const justification = entity.createJustification; if (!justification) continue; report.totalReviewed++; const status = this.getPromotionStatus(entity); const entry = { entityId: entity.id, path: entity.path, reusageCount: (entity.usedBy || []).length, reviewScheduled: justification.review_scheduled, status, }; // Check if review is due if (justification.review_scheduled) { const reviewDate = new Date(justification.review_scheduled); if (reviewDate <= now) { report.pendingReview.push(entry); } } // Categorize by promotion status if (status === 'promotion-candidate') { report.promotionCandidates.push(entry); } else if (status === 'deprecation-review') { report.deprecationReview.push(entry); } else { report.monitoring.push(entry); } } return report; } /** * Determine promotion status for an entity created via CREATE decision. * (Task 9.6 — Promotion pathway logic) * * - Used 3+ times -> promotion-candidate * - Used 1-2 times -> monitoring * - Never reused after 60 days -> deprecation-review */ getPromotionStatus(entity) { if (!entity) return 'unknown'; const reusageCount = (entity.usedBy || []).length; if (reusageCount >= 3) { return 'promotion-candidate'; } if (reusageCount >= 1) { return 'monitoring'; } // Check if 60 days have passed since creation/last verification const referenceDate = entity.createdAt || (entity.createJustification && entity.createJustification.created_at) || entity.lastVerified; if (referenceDate) { const refDate = new Date(referenceDate); const daysSince = (Date.now() - refDate.getTime()) / (1000 * 60 * 60 * 24); if (daysSince > 60) { return 'deprecation-review'; } } return 'monitoring'; } // ================================================================ // Task 7: Performance — Caching // ================================================================ /** * Clear all internal caches. */ clearCache() { this._analysisCache.clear(); this._analysisCacheTimestamps.clear(); this._idfCache = null; this._idfCacheTimestamp = 0; } _getFromCache(key) { const timestamp = this._analysisCacheTimestamps.get(key); if (timestamp && Date.now() - timestamp < CACHE_TTL_MS) { return this._analysisCache.get(key); } this._analysisCache.delete(key); this._analysisCacheTimestamps.delete(key); return null; } _setCache(key, value) { this._analysisCache.set(key, value); this._analysisCacheTimestamps.set(key, Date.now()); } // ================================================================ // Utilities // ================================================================ _round(n) { return Math.round(n * 1000) / 1000; } _pct(n) { return `${(n * 100).toFixed(1)}%`; } } module.exports = { IncrementalDecisionEngine, STOP_WORDS, THRESHOLD_MINIMUM, ADAPT_IMPACT_THRESHOLD, KEYWORD_OVERLAP_WEIGHT, PURPOSE_SIMILARITY_WEIGHT, MAX_RESULTS, CACHE_TTL_MS, };