feat(analyze): add task ID filtering to analyze-complexity command
Enhance analyze-complexity to support analyzing specific tasks by ID or range: - Add --id option for comma-separated task IDs - Add --from/--to options for analyzing tasks within a range - Implement intelligent merging with existing reports - Update CLI, MCP tools, and direct functions for consistent support - Add changeset documenting the feature
This commit is contained in:
@@ -1211,6 +1211,18 @@ function registerCommands(programInstance) {
|
||||
'-r, --research',
|
||||
'Use Perplexity AI for research-backed complexity analysis'
|
||||
)
|
||||
.option(
|
||||
'-i, --id <ids>',
|
||||
'Comma-separated list of specific task IDs to analyze (e.g., "1,3,5")'
|
||||
)
|
||||
.option(
|
||||
'--from <id>',
|
||||
'Starting task ID in a range to analyze'
|
||||
)
|
||||
.option(
|
||||
'--to <id>',
|
||||
'Ending task ID in a range to analyze'
|
||||
)
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file || 'tasks/tasks.json';
|
||||
const outputPath = options.output;
|
||||
@@ -1221,6 +1233,14 @@ function registerCommands(programInstance) {
|
||||
console.log(chalk.blue(`Analyzing task complexity from: ${tasksPath}`));
|
||||
console.log(chalk.blue(`Output report will be saved to: ${outputPath}`));
|
||||
|
||||
if (options.id) {
|
||||
console.log(chalk.blue(`Analyzing specific task IDs: ${options.id}`));
|
||||
} else if (options.from || options.to) {
|
||||
const fromStr = options.from ? options.from : 'first';
|
||||
const toStr = options.to ? options.to : 'last';
|
||||
console.log(chalk.blue(`Analyzing tasks in range: ${fromStr} to ${toStr}`));
|
||||
}
|
||||
|
||||
if (useResearch) {
|
||||
console.log(
|
||||
chalk.blue(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import readline from 'readline';
|
||||
import fs from 'fs';
|
||||
|
||||
import { log, readJSON, writeJSON, isSilentMode } from '../utils.js';
|
||||
|
||||
@@ -51,6 +52,9 @@ Do not include any explanatory text, markdown formatting, or code block markers
|
||||
* @param {string|number} [options.threshold] - Complexity threshold
|
||||
* @param {boolean} [options.research] - Use research role
|
||||
* @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
|
||||
* @param {string} [options.id] - Comma-separated list of task IDs to analyze specifically
|
||||
* @param {number} [options.from] - Starting task ID in a range to analyze
|
||||
* @param {number} [options.to] - Ending task ID in a range to analyze
|
||||
* @param {Object} [options._filteredTasksData] - Pre-filtered task data (internal use)
|
||||
* @param {number} [options._originalTaskCount] - Original task count (internal use)
|
||||
* @param {Object} context - Context object, potentially containing session and mcpLog
|
||||
@@ -65,6 +69,10 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
const thresholdScore = parseFloat(options.threshold || '5');
|
||||
const useResearch = options.research || false;
|
||||
const projectRoot = options.projectRoot;
|
||||
// New parameters for task ID filtering
|
||||
const specificIds = options.id ? options.id.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id)) : null;
|
||||
const fromId = options.from !== undefined ? parseInt(options.from, 10) : null;
|
||||
const toId = options.to !== undefined ? parseInt(options.to, 10) : null;
|
||||
|
||||
const outputFormat = mcpLog ? 'json' : 'text';
|
||||
|
||||
@@ -88,13 +96,14 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
reportLog(`Reading tasks from ${tasksPath}...`, 'info');
|
||||
let tasksData;
|
||||
let originalTaskCount = 0;
|
||||
let originalData = null;
|
||||
|
||||
if (options._filteredTasksData) {
|
||||
tasksData = options._filteredTasksData;
|
||||
originalTaskCount = options._originalTaskCount || tasksData.tasks.length;
|
||||
if (!options._originalTaskCount) {
|
||||
try {
|
||||
const originalData = readJSON(tasksPath);
|
||||
originalData = readJSON(tasksPath);
|
||||
if (originalData && originalData.tasks) {
|
||||
originalTaskCount = originalData.tasks.length;
|
||||
}
|
||||
@@ -103,22 +112,55 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tasksData = readJSON(tasksPath);
|
||||
originalData = readJSON(tasksPath);
|
||||
if (
|
||||
!tasksData ||
|
||||
!tasksData.tasks ||
|
||||
!Array.isArray(tasksData.tasks) ||
|
||||
tasksData.tasks.length === 0
|
||||
!originalData ||
|
||||
!originalData.tasks ||
|
||||
!Array.isArray(originalData.tasks) ||
|
||||
originalData.tasks.length === 0
|
||||
) {
|
||||
throw new Error('No tasks found in the tasks file');
|
||||
}
|
||||
originalTaskCount = tasksData.tasks.length;
|
||||
originalTaskCount = originalData.tasks.length;
|
||||
|
||||
// Filter tasks based on active status
|
||||
const activeStatuses = ['pending', 'blocked', 'in-progress'];
|
||||
const filteredTasks = tasksData.tasks.filter((task) =>
|
||||
let filteredTasks = originalData.tasks.filter((task) =>
|
||||
activeStatuses.includes(task.status?.toLowerCase() || 'pending')
|
||||
);
|
||||
|
||||
// Apply ID filtering if specified
|
||||
if (specificIds && specificIds.length > 0) {
|
||||
reportLog(`Filtering tasks by specific IDs: ${specificIds.join(', ')}`, 'info');
|
||||
filteredTasks = filteredTasks.filter(task => specificIds.includes(task.id));
|
||||
|
||||
if (outputFormat === 'text') {
|
||||
if (filteredTasks.length === 0 && specificIds.length > 0) {
|
||||
console.log(chalk.yellow(`Warning: No active tasks found with IDs: ${specificIds.join(', ')}`));
|
||||
} else if (filteredTasks.length < specificIds.length) {
|
||||
const foundIds = filteredTasks.map(t => t.id);
|
||||
const missingIds = specificIds.filter(id => !foundIds.includes(id));
|
||||
console.log(chalk.yellow(`Warning: Some requested task IDs were not found or are not active: ${missingIds.join(', ')}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Apply range filtering if specified
|
||||
else if (fromId !== null || toId !== null) {
|
||||
const effectiveFromId = fromId !== null ? fromId : 1;
|
||||
const effectiveToId = toId !== null ? toId : Math.max(...originalData.tasks.map(t => t.id));
|
||||
|
||||
reportLog(`Filtering tasks by ID range: ${effectiveFromId} to ${effectiveToId}`, 'info');
|
||||
filteredTasks = filteredTasks.filter(task =>
|
||||
task.id >= effectiveFromId && task.id <= effectiveToId
|
||||
);
|
||||
|
||||
if (outputFormat === 'text' && filteredTasks.length === 0) {
|
||||
console.log(chalk.yellow(`Warning: No active tasks found in range: ${effectiveFromId}-${effectiveToId}`));
|
||||
}
|
||||
}
|
||||
|
||||
tasksData = {
|
||||
...tasksData,
|
||||
...originalData,
|
||||
tasks: filteredTasks,
|
||||
_originalTaskCount: originalTaskCount
|
||||
};
|
||||
@@ -129,7 +171,18 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
`Found ${originalTaskCount} total tasks in the task file.`,
|
||||
'info'
|
||||
);
|
||||
if (skippedCount > 0) {
|
||||
|
||||
// Updated messaging to reflect filtering logic
|
||||
if (specificIds || fromId !== null || toId !== null) {
|
||||
const filterMsg = specificIds
|
||||
? `Analyzing ${tasksData.tasks.length} tasks with specific IDs: ${specificIds.join(', ')}`
|
||||
: `Analyzing ${tasksData.tasks.length} tasks in range: ${fromId || 1} to ${toId || 'end'}`;
|
||||
|
||||
reportLog(filterMsg, 'info');
|
||||
if (outputFormat === 'text') {
|
||||
console.log(chalk.blue(filterMsg));
|
||||
}
|
||||
} else if (skippedCount > 0) {
|
||||
const skipMessage = `Skipping ${skippedCount} tasks marked as done/cancelled/deferred. Analyzing ${tasksData.tasks.length} active tasks.`;
|
||||
reportLog(skipMessage, 'info');
|
||||
if (outputFormat === 'text') {
|
||||
@@ -137,7 +190,44 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for existing report before doing analysis
|
||||
let existingReport = null;
|
||||
let existingAnalysisMap = new Map(); // For quick lookups by task ID
|
||||
try {
|
||||
if (fs.existsSync(outputPath)) {
|
||||
existingReport = readJSON(outputPath);
|
||||
reportLog(`Found existing complexity report at ${outputPath}`, 'info');
|
||||
|
||||
if (existingReport &&
|
||||
existingReport.complexityAnalysis &&
|
||||
Array.isArray(existingReport.complexityAnalysis)) {
|
||||
// Create lookup map of existing analysis entries
|
||||
existingReport.complexityAnalysis.forEach(item => {
|
||||
existingAnalysisMap.set(item.taskId, item);
|
||||
});
|
||||
reportLog(`Existing report contains ${existingReport.complexityAnalysis.length} task analyses`, 'info');
|
||||
}
|
||||
}
|
||||
} catch (readError) {
|
||||
reportLog(`Warning: Could not read existing report: ${readError.message}`, 'warn');
|
||||
existingReport = null;
|
||||
existingAnalysisMap.clear();
|
||||
}
|
||||
|
||||
if (tasksData.tasks.length === 0) {
|
||||
// If using ID filtering but no matching tasks, return existing report or empty
|
||||
if (existingReport && (specificIds || fromId !== null || toId !== null)) {
|
||||
reportLog(`No matching tasks found for analysis. Keeping existing report.`, 'info');
|
||||
if (outputFormat === 'text') {
|
||||
console.log(chalk.yellow(`No matching tasks found for analysis. Keeping existing report.`));
|
||||
}
|
||||
return {
|
||||
report: existingReport,
|
||||
telemetryData: null
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise create empty report
|
||||
const emptyReport = {
|
||||
meta: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
@@ -146,9 +236,9 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
projectName: getProjectName(session),
|
||||
usedResearch: useResearch
|
||||
},
|
||||
complexityAnalysis: []
|
||||
complexityAnalysis: existingReport?.complexityAnalysis || []
|
||||
};
|
||||
reportLog(`Writing empty complexity report to ${outputPath}...`, 'info');
|
||||
reportLog(`Writing complexity report to ${outputPath}...`, 'info');
|
||||
writeJSON(outputPath, emptyReport);
|
||||
reportLog(
|
||||
`Task complexity analysis complete. Report written to ${outputPath}`,
|
||||
@@ -196,9 +286,13 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
)
|
||||
);
|
||||
}
|
||||
return emptyReport;
|
||||
return {
|
||||
report: emptyReport,
|
||||
telemetryData: null
|
||||
};
|
||||
}
|
||||
|
||||
// Continue with regular analysis path
|
||||
const prompt = generateInternalComplexityAnalysisPrompt(tasksData);
|
||||
const systemPrompt =
|
||||
'You are an expert software architect and project manager analyzing task complexity. Respond only with the requested valid JSON array.';
|
||||
@@ -326,15 +420,38 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with existing report
|
||||
let finalComplexityAnalysis = [];
|
||||
|
||||
if (existingReport && Array.isArray(existingReport.complexityAnalysis)) {
|
||||
// Create a map of task IDs that we just analyzed
|
||||
const analyzedTaskIds = new Set(complexityAnalysis.map(item => item.taskId));
|
||||
|
||||
// Keep existing entries that weren't in this analysis run
|
||||
const existingEntriesNotAnalyzed = existingReport.complexityAnalysis.filter(
|
||||
item => !analyzedTaskIds.has(item.taskId)
|
||||
);
|
||||
|
||||
// Combine with new analysis
|
||||
finalComplexityAnalysis = [...existingEntriesNotAnalyzed, ...complexityAnalysis];
|
||||
|
||||
reportLog(`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries`, 'info');
|
||||
} else {
|
||||
// No existing report or invalid format, just use the new analysis
|
||||
finalComplexityAnalysis = complexityAnalysis;
|
||||
}
|
||||
|
||||
const report = {
|
||||
meta: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
tasksAnalyzed: tasksData.tasks.length,
|
||||
totalTasks: originalTaskCount,
|
||||
analysisCount: finalComplexityAnalysis.length,
|
||||
thresholdScore: thresholdScore,
|
||||
projectName: getProjectName(session),
|
||||
usedResearch: useResearch
|
||||
},
|
||||
complexityAnalysis: complexityAnalysis
|
||||
complexityAnalysis: finalComplexityAnalysis
|
||||
};
|
||||
reportLog(`Writing complexity report to ${outputPath}...`, 'info');
|
||||
writeJSON(outputPath, report);
|
||||
@@ -350,6 +467,7 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
`Task complexity analysis complete. Report written to ${outputPath}`
|
||||
)
|
||||
);
|
||||
// Calculate statistics specifically for this analysis run
|
||||
const highComplexity = complexityAnalysis.filter(
|
||||
(t) => t.complexityScore >= 8
|
||||
).length;
|
||||
@@ -361,18 +479,21 @@ async function analyzeTaskComplexity(options, context = {}) {
|
||||
).length;
|
||||
const totalAnalyzed = complexityAnalysis.length;
|
||||
|
||||
console.log('\nComplexity Analysis Summary:');
|
||||
console.log('\nCurrent Analysis Summary:');
|
||||
console.log('----------------------------');
|
||||
console.log(
|
||||
`Active tasks sent for analysis: ${tasksData.tasks.length}`
|
||||
);
|
||||
console.log(`Tasks successfully analyzed: ${totalAnalyzed}`);
|
||||
console.log(`Tasks analyzed in this run: ${totalAnalyzed}`);
|
||||
console.log(`High complexity tasks: ${highComplexity}`);
|
||||
console.log(`Medium complexity tasks: ${mediumComplexity}`);
|
||||
console.log(`Low complexity tasks: ${lowComplexity}`);
|
||||
console.log(
|
||||
`Sum verification: ${highComplexity + mediumComplexity + lowComplexity} (should equal ${totalAnalyzed})`
|
||||
);
|
||||
|
||||
if (existingReport) {
|
||||
console.log('\nUpdated Report Summary:');
|
||||
console.log('----------------------------');
|
||||
console.log(`Total analyses in report: ${finalComplexityAnalysis.length}`);
|
||||
console.log(`Analyses from previous runs: ${finalComplexityAnalysis.length - totalAnalyzed}`);
|
||||
console.log(`New/updated analyses: ${totalAnalyzed}`);
|
||||
}
|
||||
|
||||
console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`);
|
||||
console.log(
|
||||
`\nSee ${outputPath} for the full report and expansion commands.`
|
||||
|
||||
Reference in New Issue
Block a user