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

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

2

3 from __future__ import annotations

4

5 from pathlib import Path

6 from typing import Any

7

8 from little_loops.issue_history._utils import get_issue_content

9 from little_loops.issue_history.models import (

10 CompletedIssue,

11 Hotspot,

12 HotspotAnalysis,

13)

14 from little_loops.issue_history.parsing import _extract_paths_from_issue

15

16

17 def 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 )