@@ -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