UNPKG

mcp-product-manager

Version:

MCP Orchestrator for task and project management with web interface

571 lines 22.4 kB
/** * Bundle Scoring System * * Implements prioritized scoring for task bundles based on: * - Unlock chains (highest priority) * - Semantic grouping * - Category similarity * - Task dependencies * - Resource optimization */ import { getSemanticPrefixExtractor } from './semanticPrefixExtractor.js'; class BundleScoringSystem { constructor() { this.extractor = getSemanticPrefixExtractor(); // Scoring weights (total should be ~1000 for unlock chains, ~500 for semantic, ~100 for category) this.weights = { // Primary factors (high impact) unlock_chain: { base: 1000, per_unlock: 100, critical_unlock_bonus: 200 }, // Semantic grouping (medium-high impact) semantic_group: { base: 500, per_task: 50, sequential_bonus: 100, area_feature_bonus: 100, // Bonus for [AREA-FEATURE-###] pattern high_confidence_bonus: 50 }, // Category grouping (medium impact) category_group: { base: 100, per_task: 10, bug_priority_bonus: 50, same_priority_bonus: 20 }, // Secondary factors (modifiers) priority: { critical: 200, high: 100, medium: 50, low: 0 }, efficiency: { shared_files_bonus: 30, similar_complexity_bonus: 20, optimal_size_bonus: 25 }, urgency: { overdue_penalty: -50, deadline_approaching: 100, blocked_task_penalty: -30 }, resource: { model_match_bonus: 40, skill_match_bonus: 30, context_reuse_bonus: 25 } }; // Optimal bundle sizes by model this.optimalSizes = { opus: { min: 3, optimal: 5, max: 8 }, sonnet: { min: 2, optimal: 4, max: 6 }, haiku: { min: 2, optimal: 3, max: 4 } }; } /** * Score a bundle of tasks * @param {Object} bundle - Bundle configuration * @param {Object} context - Additional context (model, agent, project state) * @returns {Object} Detailed scoring breakdown */ scoreBundle(bundle, context = {}) { const scoring = { total: 0, components: {}, confidence: 1.0, recommendation: '', optimization_hints: [] }; // 1. Score unlock chains if (bundle.bundle_type === 'unlock_chain') { scoring.components.unlock = this.scoreUnlockChain(bundle, context); scoring.total += scoring.components.unlock.score; } // 2. Score semantic grouping if (bundle.bundle_type === 'semantic_group' || bundle.semantic_prefix) { scoring.components.semantic = this.scoreSemanticGroup(bundle, context); scoring.total += scoring.components.semantic.score; } // 3. Score category grouping if (bundle.bundle_type === 'category_group' || bundle.category) { scoring.components.category = this.scoreCategoryGroup(bundle, context); scoring.total += scoring.components.category.score; } // 4. Apply priority modifiers scoring.components.priority = this.scorePriority(bundle, context); scoring.total += scoring.components.priority.score; // 5. Apply efficiency modifiers scoring.components.efficiency = this.scoreEfficiency(bundle, context); scoring.total += scoring.components.efficiency.score; // 6. Apply urgency modifiers scoring.components.urgency = this.scoreUrgency(bundle, context); scoring.total += scoring.components.urgency.score; // 7. Apply resource optimization modifiers scoring.components.resource = this.scoreResourceOptimization(bundle, context); scoring.total += scoring.components.resource.score; // Calculate confidence based on scoring components scoring.confidence = this.calculateConfidence(scoring.components); // Generate recommendation scoring.recommendation = this.generateRecommendation(bundle, scoring); // Add optimization hints scoring.optimization_hints = this.generateOptimizationHints(bundle, scoring, context); return scoring; } /** * Score unlock chain bundles */ scoreUnlockChain(bundle, context) { const result = { score: this.weights.unlock_chain.base, details: {} }; // Add score per task that will be unlocked const unlocksCount = bundle.unlocks_count || bundle.details?.unlocks?.length || 0; result.score += unlocksCount * this.weights.unlock_chain.per_unlock; result.details.unlocks_count = unlocksCount; // Check if any unlocked tasks are critical if (bundle.details?.unlocks_priorities) { const hasCritical = bundle.details.unlocks_priorities.includes('critical'); if (hasCritical) { result.score += this.weights.unlock_chain.critical_unlock_bonus; result.details.unlocks_critical = true; } } // Check chain depth (how many levels of dependencies) if (bundle.details?.chain_depth) { result.score += bundle.details.chain_depth * 50; result.details.chain_depth = bundle.details.chain_depth; } result.details.reason = `Unlocks ${unlocksCount} tasks${result.details.unlocks_critical ? ' including critical tasks' : ''}`; return result; } /** * Score semantic grouping */ scoreSemanticGroup(bundle, context) { const result = { score: this.weights.semantic_group.base, details: {} }; // Score based on number of tasks const taskCount = bundle.task_count || bundle.task_ids?.length || 0; result.score += taskCount * this.weights.semantic_group.per_task; result.details.task_count = taskCount; // Check if tasks are sequential if (bundle.details?.sequential || this.checkSequential(bundle)) { result.score += this.weights.semantic_group.sequential_bonus; result.details.is_sequential = true; } // Check for AREA-FEATURE pattern (more specific = better) const prefix = bundle.semantic_prefix || bundle.details?.semantic_prefix; if (prefix && prefix.includes('-')) { result.score += this.weights.semantic_group.area_feature_bonus; result.details.has_feature_grouping = true; } // Check confidence of semantic extraction if (bundle.details?.extraction_confidence) { if (bundle.details.extraction_confidence >= 0.8) { result.score += this.weights.semantic_group.high_confidence_bonus; result.details.high_confidence = true; } } result.details.semantic_prefix = prefix; result.details.reason = `${taskCount} related ${prefix} tasks${result.details.is_sequential ? ' (sequential)' : ''}`; return result; } /** * Score category grouping */ scoreCategoryGroup(bundle, context) { const result = { score: this.weights.category_group.base, details: {} }; // Score based on number of tasks const taskCount = bundle.task_count || bundle.task_ids?.length || 0; result.score += taskCount * this.weights.category_group.per_task; result.details.task_count = taskCount; // Bug fixes get priority const category = bundle.category || bundle.details?.category; if (category === 'bug' || category === 'bug-fix') { result.score += this.weights.category_group.bug_priority_bonus; result.details.is_bug_fix = true; } // Check if all tasks have same priority if (bundle.tasks && this.checkSamePriority(bundle.tasks)) { result.score += this.weights.category_group.same_priority_bonus; result.details.same_priority = true; } result.details.category = category; result.details.reason = `${taskCount} ${category} tasks${result.details.is_bug_fix ? ' (bug fixes)' : ''}`; return result; } /** * Score based on task priorities */ scorePriority(bundle, context) { const result = { score: 0, details: {} }; if (!bundle.tasks || bundle.tasks.length === 0) { return result; } // Calculate average priority score const priorityScores = bundle.tasks.map(task => this.weights.priority[task.priority] || this.weights.priority.medium); const avgPriorityScore = priorityScores.reduce((a, b) => a + b, 0) / priorityScores.length; result.score = Math.round(avgPriorityScore); // Find highest priority const priorities = bundle.tasks.map(t => t.priority); if (priorities.includes('critical')) { result.details.has_critical = true; result.details.highest_priority = 'critical'; } else if (priorities.includes('high')) { result.details.highest_priority = 'high'; } else if (priorities.includes('medium')) { result.details.highest_priority = 'medium'; } else { result.details.highest_priority = 'low'; } result.details.reason = `Highest priority: ${result.details.highest_priority}`; return result; } /** * Score based on efficiency factors */ scoreEfficiency(bundle, context) { const result = { score: 0, details: {} }; // Check for shared files (reduces context switching) if (bundle.details?.shared_files && bundle.details.shared_files.length > 0) { result.score += this.weights.efficiency.shared_files_bonus; result.details.has_shared_files = true; result.details.shared_files_count = bundle.details.shared_files.length; } // Check for similar complexity if (bundle.tasks && this.checkSimilarComplexity(bundle.tasks)) { result.score += this.weights.efficiency.similar_complexity_bonus; result.details.similar_complexity = true; } // Check for optimal bundle size const model = context.model || 'sonnet'; const optimalSize = this.optimalSizes[model]; const taskCount = bundle.task_count || bundle.task_ids?.length || 0; if (taskCount >= optimalSize.min && taskCount <= optimalSize.optimal) { result.score += this.weights.efficiency.optimal_size_bonus; result.details.optimal_size = true; } else if (taskCount > optimalSize.optimal && taskCount <= optimalSize.max) { result.score += this.weights.efficiency.optimal_size_bonus / 2; result.details.near_optimal_size = true; } result.details.reason = this.describeEfficiency(result.details); return result; } /** * Score based on urgency factors */ scoreUrgency(bundle, context) { const result = { score: 0, details: {} }; if (!bundle.tasks) return result; const now = new Date(); // Check for overdue tasks const overdueTasks = bundle.tasks.filter(task => { if (!task.deadline) return false; return new Date(task.deadline) < now; }); if (overdueTasks.length > 0) { result.score += this.weights.urgency.overdue_penalty * overdueTasks.length; result.details.overdue_count = overdueTasks.length; } // Check for approaching deadlines (within 24 hours) const urgentTasks = bundle.tasks.filter(task => { if (!task.deadline) return false; const deadline = new Date(task.deadline); const hoursUntil = (deadline - now) / (1000 * 60 * 60); return hoursUntil > 0 && hoursUntil <= 24; }); if (urgentTasks.length > 0) { result.score += this.weights.urgency.deadline_approaching; result.details.urgent_count = urgentTasks.length; } // Check for blocked tasks in bundle (negative score) const blockedTasks = bundle.tasks.filter(task => task.status === 'blocked'); if (blockedTasks.length > 0) { result.score += this.weights.urgency.blocked_task_penalty * blockedTasks.length; result.details.blocked_count = blockedTasks.length; } result.details.reason = this.describeUrgency(result.details); return result; } /** * Score based on resource optimization */ scoreResourceOptimization(bundle, context) { const result = { score: 0, details: {} }; // Check model match if (context.model && bundle.recommended_model) { if (context.model === bundle.recommended_model) { result.score += this.weights.resource.model_match_bonus; result.details.model_match = true; } } // Check skill match (if agent has specialization) if (context.agent_skills && bundle.required_skills) { const matchingSkills = bundle.required_skills.filter(skill => context.agent_skills.includes(skill)); if (matchingSkills.length > 0) { result.score += this.weights.resource.skill_match_bonus; result.details.skill_match = true; result.details.matching_skills = matchingSkills; } } // Check context reuse (tasks in same area/component) if (bundle.details?.context_overlap) { result.score += this.weights.resource.context_reuse_bonus; result.details.context_reuse = true; } result.details.reason = this.describeResourceOptimization(result.details); return result; } /** * Check if tasks are sequential */ checkSequential(bundle) { if (!bundle.tasks || bundle.tasks.length < 2) return false; const numbers = bundle.tasks .map(task => { const prefixInfo = this.extractor.extract(task.description); return parseInt(prefixInfo.number); }) .filter(n => !isNaN(n)) .sort((a, b) => a - b); if (numbers.length < 2) return false; // Check if mostly consecutive let consecutive = 0; for (let i = 1; i < numbers.length; i++) { if (numbers[i] - numbers[i - 1] === 1) { consecutive++; } } return consecutive >= (numbers.length - 1) * 0.7; // 70% consecutive } /** * Check if tasks have same priority */ checkSamePriority(tasks) { if (!tasks || tasks.length < 2) return true; const firstPriority = tasks[0].priority; return tasks.every(task => task.priority === firstPriority); } /** * Check if tasks have similar complexity */ checkSimilarComplexity(tasks) { if (!tasks || tasks.length < 2) return true; const hours = tasks.map(t => t.estimated_hours || 2); const avg = hours.reduce((a, b) => a + b, 0) / hours.length; const variance = hours.reduce((sum, h) => sum + Math.pow(h - avg, 2), 0) / hours.length; const stdDev = Math.sqrt(variance); // Similar if standard deviation is less than 30% of average return stdDev < avg * 0.3; } /** * Calculate confidence score */ calculateConfidence(components) { let confidence = 1.0; // Reduce confidence for missing components const expectedComponents = ['unlock', 'semantic', 'category', 'priority', 'efficiency']; const presentComponents = Object.keys(components); const missingCount = expectedComponents.filter(c => !presentComponents.includes(c)).length; confidence -= missingCount * 0.1; // Increase confidence for high scores const totalScore = Object.values(components).reduce((sum, c) => sum + (c.score || 0), 0); if (totalScore > 1000) confidence += 0.1; if (totalScore > 1500) confidence += 0.1; return Math.max(0, Math.min(1, confidence)); } /** * Generate recommendation text */ generateRecommendation(bundle, scoring) { const totalScore = scoring.total; const components = scoring.components; let recommendation = ''; let emoji = ''; if (totalScore >= 1500) { emoji = '🏆'; recommendation = 'CRITICAL PRIORITY - '; } else if (totalScore >= 1000) { emoji = '🔥'; recommendation = 'HIGH PRIORITY - '; } else if (totalScore >= 500) { emoji = '✨'; recommendation = 'RECOMMENDED - '; } else if (totalScore >= 200) { emoji = '👍'; recommendation = 'GOOD CHOICE - '; } else { emoji = '📦'; recommendation = 'AVAILABLE - '; } // Add primary reason if (components.unlock?.score > 0) { recommendation += `Unlocks ${components.unlock.details.unlocks_count} tasks`; } else if (components.semantic?.score > 0) { recommendation += `Efficient ${components.semantic.details.semantic_prefix} grouping`; } else if (components.category?.score > 0) { recommendation += `${components.category.details.category} batch work`; } else { recommendation += 'General task bundle'; } // Add modifiers const modifiers = []; if (components.priority?.details.has_critical) { modifiers.push('critical tasks'); } if (components.efficiency?.details.optimal_size) { modifiers.push('optimal size'); } if (components.urgency?.details.urgent_count) { modifiers.push('urgent deadline'); } if (modifiers.length > 0) { recommendation += ` (${modifiers.join(', ')})`; } return emoji + ' ' + recommendation; } /** * Generate optimization hints */ generateOptimizationHints(bundle, scoring, context) { const hints = []; const taskCount = bundle.task_count || bundle.task_ids?.length || 0; const model = context.model || 'sonnet'; const optimalSize = this.optimalSizes[model]; // Size optimization if (taskCount < optimalSize.min) { hints.push(`Add ${optimalSize.min - taskCount} more tasks to reach minimum bundle size`); } else if (taskCount > optimalSize.max) { hints.push(`Split into multiple bundles (current: ${taskCount}, max: ${optimalSize.max})`); } // Priority optimization if (!scoring.components.priority?.details.has_critical && context.has_critical_tasks) { hints.push('Consider including critical priority tasks'); } // Efficiency optimization if (!scoring.components.efficiency?.details.has_shared_files) { hints.push('Look for tasks that modify the same files for better context reuse'); } // Unlock optimization if (!scoring.components.unlock && context.has_blocked_tasks) { hints.push('Prioritize tasks that unlock blocked work'); } return hints; } /** * Helper function to describe efficiency */ describeEfficiency(details) { const parts = []; if (details.has_shared_files) { parts.push(`${details.shared_files_count} shared files`); } if (details.similar_complexity) { parts.push('similar complexity'); } if (details.optimal_size) { parts.push('optimal size'); } return parts.join(', ') || 'Standard efficiency'; } /** * Helper function to describe urgency */ describeUrgency(details) { const parts = []; if (details.overdue_count) { parts.push(`${details.overdue_count} overdue`); } if (details.urgent_count) { parts.push(`${details.urgent_count} urgent`); } if (details.blocked_count) { parts.push(`${details.blocked_count} blocked`); } return parts.join(', ') || 'No urgency factors'; } /** * Helper function to describe resource optimization */ describeResourceOptimization(details) { const parts = []; if (details.model_match) { parts.push('model match'); } if (details.skill_match) { parts.push('skill match'); } if (details.context_reuse) { parts.push('context reuse'); } return parts.join(', ') || 'Standard resource usage'; } /** * Compare two bundles */ compareB; undles(bundleA, bundleB, context = {}) { const scoreA = this.scoreBundle(bundleA, context); const scoreB = this.scoreBundle(bundleB, context); return { winner: scoreA.total > scoreB.total ? 'A' : 'B', scoreA: scoreA.total, scoreB: scoreB.total, difference: Math.abs(scoreA.total - scoreB.total), recommendationA: scoreA.recommendation, recommendationB: scoreB.recommendation }; } } // Singleton instance let scoringInstance = null; export function getBundleScoringSystem() { if (!scoringInstance) { scoringInstance = new BundleScoringSystem(); } return scoringInstance; } export default BundleScoringSystem; //# sourceMappingURL=bundleScoring.js.map