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
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
1"""Issue history hotspot detection analysis."""
3from __future__ import annotations
5from pathlib import Path
6from typing import Any
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
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.
23 Args:
24 issues: List of completed issues
25 contents: Pre-loaded issue file contents (path -> content)
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}
33 for issue in issues:
34 content = get_issue_content(issue, contents)
35 if content is None:
36 continue
38 paths = _extract_paths_from_issue(content)
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 )
50 # Track directory hotspot
51 if "/" in path:
52 dir_path = "/".join(path.split("/")[:-1]) + "/"
53 else:
54 dir_path = "./"
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 )
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
72 # Determine churn indicator
73 if total >= 5:
74 churn = "high"
75 elif total >= 3:
76 churn = "medium"
77 else:
78 churn = "low"
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 )
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
98 if total >= 5:
99 churn = "high"
100 elif total >= 3:
101 churn = "medium"
102 else:
103 churn = "low"
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 )
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)
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))
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 )