Skip to content

Commit 52d3e08

Browse files
committed
fix: Preserve complexity report entries across --from/--to range runs
The merge logic was filtering existing report entries against tasksData.tasks, which is already filtered by --from/--to range. This caused entries from previous runs outside the current range to be dropped. Since report files are tag-specific, the tag membership check was redundant — removed it so all existing entries not in the current analysis are preserved. Closes #1644
1 parent d56b0c9 commit 52d3e08

File tree

3 files changed

+239
-8
lines changed

3 files changed

+239
-8
lines changed

.changeset/fix-complexity-merge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"task-master-ai": patch
3+
---
4+
5+
Fix complexity report losing results when running analyze-complexity with --from/--to ranges multiple times. Previous entries outside the current analysis range are now preserved across runs.

scripts/modules/task-manager/analyze-task-complexity.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,9 @@ async function analyzeTaskComplexity(options, context = {}) {
467467
}
468468
}
469469

470-
// Merge with existing report - only keep entries from the current tag
470+
// Merge with existing report - preserve entries from previous runs.
471+
// Report files are already tag-specific (e.g., task-complexity-report_<tag>.json),
472+
// so all entries in the file belong to the current tag by definition.
471473
let finalComplexityAnalysis = [];
472474

473475
if (existingReport && Array.isArray(existingReport.complexityAnalysis)) {
@@ -476,14 +478,11 @@ async function analyzeTaskComplexity(options, context = {}) {
476478
complexityAnalysis.map((item) => item.taskId)
477479
);
478480

479-
// Keep existing entries that weren't in this analysis run AND belong to the current tag
480-
// We determine tag membership by checking if the task ID exists in the current tag's tasks
481-
const currentTagTaskIds = new Set(tasksData.tasks.map((t) => t.id));
481+
// Keep existing entries that weren't in this analysis run.
482+
// Since report files are tag-specific, all entries belong to this tag.
482483
const existingEntriesNotAnalyzed =
483484
existingReport.complexityAnalysis.filter(
484-
(item) =>
485-
!analyzedTaskIds.has(item.taskId) &&
486-
currentTagTaskIds.has(item.taskId) // Only keep entries for tasks in current tag
485+
(item) => !analyzedTaskIds.has(item.taskId)
487486
);
488487

489488
// Combine with new analysis
@@ -493,7 +492,7 @@ async function analyzeTaskComplexity(options, context = {}) {
493492
];
494493

495494
reportLog(
496-
`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries from current tag`,
495+
`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries`,
497496
'info'
498497
);
499498
} else {

tests/unit/scripts/modules/task-manager/complexity-report-tag-isolation.test.js

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,233 @@ describe('Complexity Report Tag Isolation', () => {
11291129
});
11301130
});
11311131

1132+
describe('Report Merge With --from/--to Ranges', () => {
1133+
test('should preserve existing entries outside the current analysis range', async () => {
1134+
// Simulate: Run 1 analyzed tasks 1-2, Run 2 analyzes tasks 3-4.
1135+
// After Run 2, tasks 1-2 should still be in the report.
1136+
const tasksForTag = {
1137+
tasks: [
1138+
{ id: 1, title: 'Task 1', description: 'First task', status: 'pending' },
1139+
{ id: 2, title: 'Task 2', description: 'Second task', status: 'pending' },
1140+
{ id: 3, title: 'Task 3', description: 'Third task', status: 'pending' },
1141+
{ id: 4, title: 'Task 4', description: 'Fourth task', status: 'pending' }
1142+
]
1143+
};
1144+
1145+
readJSON.mockReturnValue(tasksForTag);
1146+
1147+
// Existing report from "Run 1" that analyzed tasks 1-2
1148+
const existingReport = {
1149+
meta: { generatedAt: new Date().toISOString(), tasksAnalyzed: 2 },
1150+
complexityAnalysis: [
1151+
{
1152+
taskId: 1,
1153+
taskTitle: 'Task 1',
1154+
complexityScore: 7,
1155+
recommendedSubtasks: 4,
1156+
expansionPrompt: 'Break down task 1',
1157+
reasoning: 'From run 1'
1158+
},
1159+
{
1160+
taskId: 2,
1161+
taskTitle: 'Task 2',
1162+
complexityScore: 5,
1163+
recommendedSubtasks: 3,
1164+
expansionPrompt: 'Break down task 2',
1165+
reasoning: 'From run 1'
1166+
}
1167+
]
1168+
};
1169+
1170+
// Mock: existing report file exists and returns run-1 data
1171+
mockExistsSync.mockReturnValue(true);
1172+
mockReadFileSync.mockReturnValue(JSON.stringify(existingReport));
1173+
1174+
// Mock AI response for "Run 2" (tasks 3-4 only)
1175+
generateObjectService.mockResolvedValueOnce({
1176+
mainResult: {
1177+
complexityAnalysis: [
1178+
{
1179+
taskId: 3,
1180+
taskTitle: 'Task 3',
1181+
complexityScore: 8,
1182+
recommendedSubtasks: 5,
1183+
expansionPrompt: 'Break down task 3',
1184+
reasoning: 'From run 2'
1185+
},
1186+
{
1187+
taskId: 4,
1188+
taskTitle: 'Task 4',
1189+
complexityScore: 6,
1190+
recommendedSubtasks: 4,
1191+
expansionPrompt: 'Break down task 4',
1192+
reasoning: 'From run 2'
1193+
}
1194+
]
1195+
},
1196+
telemetryData: {
1197+
timestamp: new Date().toISOString(),
1198+
commandName: 'analyze-complexity',
1199+
modelUsed: 'claude-3-5-sonnet',
1200+
providerName: 'anthropic',
1201+
inputTokens: 1000,
1202+
outputTokens: 500,
1203+
totalTokens: 1500,
1204+
totalCost: 0.012414,
1205+
currency: 'USD'
1206+
}
1207+
});
1208+
1209+
// Run 2: analyze only tasks 3-4
1210+
const options = {
1211+
file: 'tasks/tasks.json',
1212+
threshold: '5',
1213+
projectRoot,
1214+
tag: 'master',
1215+
from: 3,
1216+
to: 4
1217+
};
1218+
1219+
await analyzeTaskComplexity(options, {
1220+
projectRoot,
1221+
mcpLog: {
1222+
info: jest.fn(),
1223+
warn: jest.fn(),
1224+
error: jest.fn(),
1225+
debug: jest.fn(),
1226+
success: jest.fn()
1227+
}
1228+
});
1229+
1230+
// Verify the written report contains ALL 4 tasks (merged)
1231+
expect(mockWriteFileSync).toHaveBeenCalled();
1232+
const writtenData = JSON.parse(mockWriteFileSync.mock.calls[0][1]);
1233+
const taskIds = writtenData.complexityAnalysis.map((a) => a.taskId).sort();
1234+
1235+
expect(taskIds).toEqual([1, 2, 3, 4]);
1236+
1237+
// Verify tasks 1-2 are preserved from run 1
1238+
const task1 = writtenData.complexityAnalysis.find((a) => a.taskId === 1);
1239+
expect(task1.reasoning).toBe('From run 1');
1240+
1241+
// Verify tasks 3-4 are from run 2
1242+
const task3 = writtenData.complexityAnalysis.find((a) => a.taskId === 3);
1243+
expect(task3.reasoning).toBe('From run 2');
1244+
});
1245+
1246+
test('should update existing entries when re-analyzed in overlapping range', async () => {
1247+
const tasksForTag = {
1248+
tasks: [
1249+
{ id: 1, title: 'Task 1', description: 'First task', status: 'pending' },
1250+
{ id: 2, title: 'Task 2', description: 'Second task', status: 'pending' },
1251+
{ id: 3, title: 'Task 3', description: 'Third task', status: 'pending' }
1252+
]
1253+
};
1254+
1255+
readJSON.mockReturnValue(tasksForTag);
1256+
1257+
// Existing report with tasks 1-2
1258+
const existingReport = {
1259+
meta: { generatedAt: new Date().toISOString(), tasksAnalyzed: 2 },
1260+
complexityAnalysis: [
1261+
{
1262+
taskId: 1,
1263+
taskTitle: 'Task 1',
1264+
complexityScore: 5,
1265+
recommendedSubtasks: 3,
1266+
expansionPrompt: 'Old prompt',
1267+
reasoning: 'Old analysis'
1268+
},
1269+
{
1270+
taskId: 2,
1271+
taskTitle: 'Task 2',
1272+
complexityScore: 3,
1273+
recommendedSubtasks: 2,
1274+
expansionPrompt: 'Old prompt',
1275+
reasoning: 'Old analysis'
1276+
}
1277+
]
1278+
};
1279+
1280+
mockExistsSync.mockReturnValue(true);
1281+
mockReadFileSync.mockReturnValue(JSON.stringify(existingReport));
1282+
1283+
// AI response for overlapping range (tasks 2-3)
1284+
generateObjectService.mockResolvedValueOnce({
1285+
mainResult: {
1286+
complexityAnalysis: [
1287+
{
1288+
taskId: 2,
1289+
taskTitle: 'Task 2',
1290+
complexityScore: 8,
1291+
recommendedSubtasks: 5,
1292+
expansionPrompt: 'Updated prompt',
1293+
reasoning: 'Updated analysis'
1294+
},
1295+
{
1296+
taskId: 3,
1297+
taskTitle: 'Task 3',
1298+
complexityScore: 6,
1299+
recommendedSubtasks: 4,
1300+
expansionPrompt: 'New prompt',
1301+
reasoning: 'New analysis'
1302+
}
1303+
]
1304+
},
1305+
telemetryData: {
1306+
timestamp: new Date().toISOString(),
1307+
commandName: 'analyze-complexity',
1308+
modelUsed: 'claude-3-5-sonnet',
1309+
providerName: 'anthropic',
1310+
inputTokens: 1000,
1311+
outputTokens: 500,
1312+
totalTokens: 1500,
1313+
totalCost: 0.012414,
1314+
currency: 'USD'
1315+
}
1316+
});
1317+
1318+
const options = {
1319+
file: 'tasks/tasks.json',
1320+
threshold: '5',
1321+
projectRoot,
1322+
tag: 'master',
1323+
from: 2,
1324+
to: 3
1325+
};
1326+
1327+
await analyzeTaskComplexity(options, {
1328+
projectRoot,
1329+
mcpLog: {
1330+
info: jest.fn(),
1331+
warn: jest.fn(),
1332+
error: jest.fn(),
1333+
debug: jest.fn(),
1334+
success: jest.fn()
1335+
}
1336+
});
1337+
1338+
const writtenData = JSON.parse(mockWriteFileSync.mock.calls[0][1]);
1339+
const taskIds = writtenData.complexityAnalysis.map((a) => a.taskId).sort();
1340+
1341+
// All 3 tasks should be present
1342+
expect(taskIds).toEqual([1, 2, 3]);
1343+
1344+
// Task 1 preserved from old report
1345+
const task1 = writtenData.complexityAnalysis.find((a) => a.taskId === 1);
1346+
expect(task1.reasoning).toBe('Old analysis');
1347+
1348+
// Task 2 updated with new analysis
1349+
const task2 = writtenData.complexityAnalysis.find((a) => a.taskId === 2);
1350+
expect(task2.reasoning).toBe('Updated analysis');
1351+
expect(task2.complexityScore).toBe(8);
1352+
1353+
// Task 3 added from new analysis
1354+
const task3 = writtenData.complexityAnalysis.find((a) => a.taskId === 3);
1355+
expect(task3.reasoning).toBe('New analysis');
1356+
});
1357+
});
1358+
11321359
describe('Edge Cases', () => {
11331360
test('should handle empty tag gracefully', async () => {
11341361
const options = {

0 commit comments

Comments
 (0)