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

73 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-29 00:54 -0500

1"""Issue history analysis orchestrator. 

2 

3Thin facade that coordinates all analysis sub-modules and returns 

4a comprehensive HistoryAnalysis result. 

5""" 

6 

7from __future__ import annotations 

8 

9from datetime import date, timedelta 

10from pathlib import Path 

11from typing import Literal 

12 

13from little_loops.issue_history.coupling import analyze_coupling 

14from little_loops.issue_history.debt import ( 

15 _calculate_debt_metrics, 

16 analyze_agent_effectiveness, 

17 analyze_complexity_proxy, 

18 detect_cross_cutting_smells, 

19) 

20from little_loops.issue_history.evolution import detect_recurring_feedback, detect_skill_bypass 

21from little_loops.issue_history.hotspots import analyze_hotspots 

22from little_loops.issue_history.models import ( 

23 CompletedIssue, 

24 HistoryAnalysis, 

25 PeriodMetrics, 

26) 

27from little_loops.issue_history.parsing import scan_active_issues 

28from little_loops.issue_history.quality import ( 

29 analyze_rejection_rates, 

30 analyze_test_gaps, 

31 detect_config_gaps, 

32 detect_manual_patterns, 

33) 

34from little_loops.issue_history.regressions import analyze_regression_clustering 

35from little_loops.issue_history.summary import ( 

36 _analyze_subsystems, 

37 _calculate_trend, 

38 _group_by_period, 

39 calculate_summary, 

40) 

41from little_loops.session_store import DEFAULT_DB_PATH 

42 

43 

44def _load_issue_contents(issues: list[CompletedIssue]) -> dict[Path, str]: 

45 """Pre-load issue file contents for pipeline efficiency. 

46 

47 Reads each issue file once and returns a mapping from path to content. 

48 Skips unreadable files silently (matching individual function behavior). 

49 

50 Args: 

51 issues: List of completed issues to load 

52 

53 Returns: 

54 Mapping of issue path to file content 

55 """ 

56 contents: dict[Path, str] = {} 

57 for issue in issues: 

58 try: 

59 contents[issue.path] = issue.path.read_text(encoding="utf-8") 

60 except Exception: 

61 pass 

62 return contents 

63 

64 

65def calculate_analysis( 

66 completed_issues: list[CompletedIssue], 

67 issues_dir: Path | None = None, 

68 period_type: Literal["weekly", "monthly", "quarterly"] = "monthly", 

69 compare_days: int | None = None, 

70 project_root: Path | None = None, 

71 db_path: Path | None = None, 

72) -> HistoryAnalysis: 

73 """Calculate comprehensive history analysis. 

74 

75 Args: 

76 completed_issues: List of completed issues 

77 issues_dir: Path to .issues/ for active issue scanning 

78 period_type: Grouping period for trend analysis 

79 compare_days: Days for comparative analysis (e.g., 30 for 30d comparison) 

80 project_root: Project root for config gap analysis (defaults to cwd) 

81 

82 Returns: 

83 HistoryAnalysis with all metrics 

84 """ 

85 today = date.today() 

86 

87 # Pre-load issue file contents once for all analysis functions 

88 issue_contents = _load_issue_contents(completed_issues) 

89 

90 # Get base summary 

91 summary = calculate_summary(completed_issues) 

92 

93 # Scan active issues if directory provided 

94 active_issues: list[tuple[Path, str, str, date | None]] = [] 

95 if issues_dir: 

96 active_issues = scan_active_issues(issues_dir) 

97 

98 # Calculate period metrics 

99 period_metrics = _group_by_period(completed_issues, period_type) 

100 

101 # Determine velocity trend 

102 if len(period_metrics) >= 3: 

103 velocities = [float(p.total_completed) for p in period_metrics] 

104 velocity_trend = _calculate_trend(velocities) 

105 else: 

106 velocity_trend = "stable" 

107 

108 # Determine bug ratio trend 

109 if len(period_metrics) >= 3: 

110 bug_ratios = [p.bug_ratio or 0.0 for p in period_metrics] 

111 # For bug ratio, decreasing is good (keep as-is) 

112 bug_ratio_trend = _calculate_trend(bug_ratios) 

113 else: 

114 bug_ratio_trend = "stable" 

115 

116 # Subsystem health 

117 subsystem_health = _analyze_subsystems(completed_issues, contents=issue_contents) 

118 

119 # Hotspot analysis 

120 hotspot_analysis = analyze_hotspots(completed_issues, contents=issue_contents) 

121 

122 # Coupling analysis 

123 coupling_analysis = analyze_coupling(completed_issues, contents=issue_contents) 

124 

125 # Regression clustering analysis 

126 regression_analysis = analyze_regression_clustering(completed_issues, contents=issue_contents) 

127 

128 # Test gap analysis 

129 test_gap_analysis = analyze_test_gaps( 

130 completed_issues, hotspot_analysis, project_root=project_root 

131 ) 

132 

133 # Rejection rate analysis 

134 rejection_analysis = analyze_rejection_rates(completed_issues, contents=issue_contents) 

135 

136 # Manual pattern analysis 

137 manual_pattern_analysis = detect_manual_patterns(completed_issues, contents=issue_contents) 

138 

139 # Evolution trigger analysis (ENH-1911) 

140 from little_loops.config.features import EvolutionConfig 

141 

142 _resolved_db = db_path if db_path is not None else Path(DEFAULT_DB_PATH) 

143 _evolution_config = EvolutionConfig() 

144 recurring_feedback_analysis = detect_recurring_feedback( 

145 _resolved_db, _evolution_config, project_root=project_root 

146 ) 

147 skill_bypass_analysis = detect_skill_bypass( 

148 _resolved_db, _evolution_config, project_root=project_root 

149 ) 

150 

151 # Agent effectiveness analysis 

152 agent_effectiveness_analysis = analyze_agent_effectiveness( 

153 completed_issues, contents=issue_contents 

154 ) 

155 

156 # Complexity proxy analysis 

157 complexity_proxy_analysis = analyze_complexity_proxy( 

158 completed_issues, hotspot_analysis, contents=issue_contents 

159 ) 

160 

161 # Configuration gaps analysis (depends on manual_pattern_analysis) 

162 config_gaps_analysis = detect_config_gaps(manual_pattern_analysis, project_root) 

163 

164 # Cross-cutting concern analysis (depends on hotspot_analysis) 

165 cross_cutting_analysis = detect_cross_cutting_smells( 

166 completed_issues, hotspot_analysis, contents=issue_contents 

167 ) 

168 

169 # Technical debt metrics 

170 debt_metrics = _calculate_debt_metrics(completed_issues, active_issues) 

171 

172 # Build analysis 

173 analysis = HistoryAnalysis( 

174 generated_date=today, 

175 total_completed=len(completed_issues), 

176 total_active=len(active_issues), 

177 date_range_start=summary.earliest_date, 

178 date_range_end=summary.latest_date, 

179 summary=summary, 

180 period_metrics=period_metrics, 

181 velocity_trend=velocity_trend, 

182 bug_ratio_trend=bug_ratio_trend, 

183 subsystem_health=subsystem_health, 

184 hotspot_analysis=hotspot_analysis, 

185 coupling_analysis=coupling_analysis, 

186 regression_analysis=regression_analysis, 

187 test_gap_analysis=test_gap_analysis, 

188 rejection_analysis=rejection_analysis, 

189 manual_pattern_analysis=manual_pattern_analysis, 

190 agent_effectiveness_analysis=agent_effectiveness_analysis, 

191 complexity_proxy_analysis=complexity_proxy_analysis, 

192 config_gaps_analysis=config_gaps_analysis, 

193 cross_cutting_analysis=cross_cutting_analysis, 

194 recurring_feedback_analysis=recurring_feedback_analysis, 

195 skill_bypass_analysis=skill_bypass_analysis, 

196 debt_metrics=debt_metrics, 

197 ) 

198 

199 # Comparative analysis 

200 if compare_days: 

201 analysis.comparison_period = f"{compare_days}d" 

202 cutoff = today - timedelta(days=compare_days) 

203 prev_cutoff = cutoff - timedelta(days=compare_days) 

204 

205 current_issues = [ 

206 i for i in completed_issues if i.completed_date and i.completed_date >= cutoff 

207 ] 

208 previous_issues = [ 

209 i 

210 for i in completed_issues 

211 if i.completed_date and prev_cutoff <= i.completed_date < cutoff 

212 ] 

213 

214 if current_issues: 

215 current_types: dict[str, int] = {} 

216 for i in current_issues: 

217 current_types[i.issue_type] = current_types.get(i.issue_type, 0) + 1 

218 

219 analysis.current_period = PeriodMetrics( 

220 period_start=cutoff, 

221 period_end=today, 

222 period_label=f"Last {compare_days} days", 

223 total_completed=len(current_issues), 

224 type_counts=current_types, 

225 ) 

226 

227 if previous_issues: 

228 prev_types: dict[str, int] = {} 

229 for i in previous_issues: 

230 prev_types[i.issue_type] = prev_types.get(i.issue_type, 0) + 1 

231 

232 analysis.previous_period = PeriodMetrics( 

233 period_start=prev_cutoff, 

234 period_end=cutoff - timedelta(days=1), 

235 period_label=f"Previous {compare_days} days", 

236 total_completed=len(previous_issues), 

237 type_counts=prev_types, 

238 ) 

239 

240 return analysis