Coverage for little_loops / issue_history / hotspots.py: 0%

56 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-03-18 16:18 -0500

1"""Issue history hotspot detection analysis.""" 

2 

3from __future__ import annotations 

4 

5from pathlib import Path 

6from typing import Any 

7 

8from little_loops.issue_history._utils import get_issue_content 

9from little_loops.issue_history.models import ( 

10 CompletedIssue, 

11 Hotspot, 

12 HotspotAnalysis, 

13) 

14from little_loops.issue_history.parsing import _extract_paths_from_issue 

15 

16 

17def analyze_hotspots( 

18 issues: list[CompletedIssue], 

19 contents: dict[Path, str] | None = None, 

20) -> HotspotAnalysis: 

21 """Identify files and directories that appear repeatedly in issues. 

22 

23 Args: 

24 issues: List of completed issues 

25 contents: Pre-loaded issue file contents (path -> content) 

26 

27 Returns: 

28 HotspotAnalysis with file and directory hotspots 

29 """ 

30 file_data: dict[str, dict[str, Any]] = {} # path -> {count, ids, types} 

31 dir_data: dict[str, dict[str, Any]] = {} # dir -> {count, ids, types} 

32 

33 for issue in issues: 

34 content = get_issue_content(issue, contents) 

35 if content is None: 

36 continue 

37 

38 paths = _extract_paths_from_issue(content) 

39 

40 for path in paths: 

41 # Track file hotspot 

42 if path not in file_data: 

43 file_data[path] = {"count": 0, "ids": [], "types": {}} 

44 file_data[path]["count"] += 1 

45 file_data[path]["ids"].append(issue.issue_id) 

46 file_data[path]["types"][issue.issue_type] = ( 

47 file_data[path]["types"].get(issue.issue_type, 0) + 1 

48 ) 

49 

50 # Track directory hotspot 

51 if "/" in path: 

52 dir_path = "/".join(path.split("/")[:-1]) + "/" 

53 else: 

54 dir_path = "./" 

55 

56 if dir_path not in dir_data: 

57 dir_data[dir_path] = {"count": 0, "ids": [], "types": {}} 

58 if issue.issue_id not in dir_data[dir_path]["ids"]: 

59 dir_data[dir_path]["count"] += 1 

60 dir_data[dir_path]["ids"].append(issue.issue_id) 

61 dir_data[dir_path]["types"][issue.issue_type] = ( 

62 dir_data[dir_path]["types"].get(issue.issue_type, 0) + 1 

63 ) 

64 

65 # Convert to Hotspot objects 

66 file_hotspots: list[Hotspot] = [] 

67 for path, data in file_data.items(): 

68 bug_count = data["types"].get("BUG", 0) 

69 total = data["count"] 

70 bug_ratio = bug_count / total if total > 0 else 0.0 

71 

72 # Determine churn indicator 

73 if total >= 5: 

74 churn = "high" 

75 elif total >= 3: 

76 churn = "medium" 

77 else: 

78 churn = "low" 

79 

80 file_hotspots.append( 

81 Hotspot( 

82 path=path, 

83 issue_count=total, 

84 issue_ids=data["ids"], 

85 issue_types=data["types"], 

86 bug_ratio=bug_ratio, 

87 churn_indicator=churn, 

88 ) 

89 ) 

90 

91 # Convert directory data to Hotspot objects 

92 dir_hotspots: list[Hotspot] = [] 

93 for path, data in dir_data.items(): 

94 bug_count = data["types"].get("BUG", 0) 

95 total = data["count"] 

96 bug_ratio = bug_count / total if total > 0 else 0.0 

97 

98 if total >= 5: 

99 churn = "high" 

100 elif total >= 3: 

101 churn = "medium" 

102 else: 

103 churn = "low" 

104 

105 dir_hotspots.append( 

106 Hotspot( 

107 path=path, 

108 issue_count=total, 

109 issue_ids=data["ids"], 

110 issue_types=data["types"], 

111 bug_ratio=bug_ratio, 

112 churn_indicator=churn, 

113 ) 

114 ) 

115 

116 # Sort by issue count descending 

117 file_hotspots.sort(key=lambda h: -h.issue_count) 

118 dir_hotspots.sort(key=lambda h: -h.issue_count) 

119 

120 # Identify bug magnets (>60% bug ratio, at least 3 issues) 

121 bug_magnets = [h for h in file_hotspots if h.bug_ratio > 0.6 and h.issue_count >= 3] 

122 bug_magnets.sort(key=lambda h: (-h.bug_ratio, -h.issue_count)) 

123 

124 return HotspotAnalysis( 

125 file_hotspots=file_hotspots[:10], # Top 10 

126 directory_hotspots=dir_hotspots[:10], # Top 10 

127 bug_magnets=bug_magnets[:5], # Top 5 

128 )