mcp-product-manager
Version:
MCP Orchestrator for task and project management with web interface
571 lines • 22.4 kB
JavaScript
/**
* 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