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

659 statements  

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

1"""Issue history formatting functions. 

2 

3Provides functions to format issue history summaries and analyses 

4as text, JSON, YAML, and Markdown. 

5""" 

6 

7from __future__ import annotations 

8 

9import json 

10 

11from little_loops.issue_history.models import ( 

12 AgentOutcome, 

13 HistoryAnalysis, 

14 HistorySummary, 

15) 

16 

17 

18def format_summary_text(summary: HistorySummary) -> str: 

19 """Format summary as human-readable text. 

20 

21 Args: 

22 summary: HistorySummary to format 

23 

24 Returns: 

25 Formatted text string 

26 """ 

27 lines: list[str] = [] 

28 

29 lines.append("Issue History Summary") 

30 lines.append("=" * 21) 

31 lines.append(f"Total Completed: {summary.total_count}") 

32 

33 if summary.earliest_date and summary.latest_date: 

34 days = summary.date_range_days or 0 

35 lines.append(f"Date Range: {summary.earliest_date} to {summary.latest_date} ({days} days)") 

36 if summary.velocity: 

37 lines.append(f"Velocity: {summary.velocity:.1f} issues/day") 

38 

39 lines.append("") 

40 lines.append("By Type:") 

41 total = summary.total_count or 1 

42 for issue_type, count in summary.type_counts.items(): 

43 pct = count * 100 // total 

44 lines.append(f" {issue_type:5}: {count:3} ({pct:2}%)") 

45 

46 lines.append("") 

47 lines.append("By Priority:") 

48 for priority, count in summary.priority_counts.items(): 

49 pct = count * 100 // total 

50 lines.append(f" {priority}: {count:3} ({pct:2}%)") 

51 

52 lines.append("") 

53 lines.append("By Discovery Source:") 

54 for source, count in summary.discovery_counts.items(): 

55 pct = count * 100 // total 

56 lines.append(f" {source:15}: {count:3} ({pct:2}%)") 

57 

58 return "\n".join(lines) 

59 

60 

61def format_summary_json(summary: HistorySummary) -> str: 

62 """Format summary as JSON. 

63 

64 Args: 

65 summary: HistorySummary to format 

66 

67 Returns: 

68 JSON string 

69 """ 

70 return json.dumps(summary.to_dict(), indent=2) 

71 

72 

73def format_analysis_json(analysis: HistoryAnalysis) -> str: 

74 """Format analysis as JSON. 

75 

76 Args: 

77 analysis: HistoryAnalysis to format 

78 

79 Returns: 

80 JSON string 

81 """ 

82 return json.dumps(analysis.to_dict(), indent=2) 

83 

84 

85def format_analysis_yaml(analysis: HistoryAnalysis) -> str: 

86 """Format analysis as YAML. 

87 

88 Args: 

89 analysis: HistoryAnalysis to format 

90 

91 Returns: 

92 YAML string (falls back to JSON if yaml not available) 

93 """ 

94 try: 

95 import yaml 

96 

97 return yaml.dump(analysis.to_dict(), default_flow_style=False, sort_keys=False) 

98 except ImportError: 

99 # Fallback to JSON if yaml not available 

100 return format_analysis_json(analysis) 

101 

102 

103def format_analysis_text(analysis: HistoryAnalysis) -> str: 

104 """Format analysis as human-readable text. 

105 

106 Args: 

107 analysis: HistoryAnalysis to format 

108 

109 Returns: 

110 Formatted text string 

111 """ 

112 lines: list[str] = [] 

113 

114 lines.append("Issue History Analysis") 

115 lines.append("=" * 22) 

116 lines.append(f"Generated: {analysis.generated_date}") 

117 lines.append(f"Completed: {analysis.total_completed} | Active: {analysis.total_active}") 

118 

119 if analysis.date_range_start and analysis.date_range_end: 

120 lines.append(f"Date Range: {analysis.date_range_start} to {analysis.date_range_end}") 

121 

122 # Summary 

123 lines.append("") 

124 lines.append("Summary") 

125 lines.append("-" * 7) 

126 summary = analysis.summary 

127 if summary.velocity: 

128 lines.append(f"Velocity: {summary.velocity:.2f} issues/day") 

129 lines.append(f"Velocity Trend: {analysis.velocity_trend}") 

130 lines.append(f"Bug Ratio Trend: {analysis.bug_ratio_trend}") 

131 

132 # Type distribution 

133 lines.append("") 

134 lines.append("By Type:") 

135 total = analysis.total_completed or 1 

136 for issue_type, count in summary.type_counts.items(): 

137 pct = count * 100 // total 

138 lines.append(f" {issue_type:5}: {count:3} ({pct:2}%)") 

139 

140 # Period metrics 

141 if analysis.period_metrics: 

142 lines.append("") 

143 lines.append("Period Metrics") 

144 lines.append("-" * 14) 

145 for period in analysis.period_metrics[-6:]: # Last 6 periods 

146 bug_pct = f"{period.bug_ratio * 100:.0f}%" if period.bug_ratio else "N/A" 

147 lines.append( 

148 f" {period.period_label:12}: {period.total_completed:3} completed, {bug_pct} bugs" 

149 ) 

150 

151 # Subsystem health 

152 if analysis.subsystem_health: 

153 lines.append("") 

154 lines.append("Subsystem Health") 

155 lines.append("-" * 16) 

156 for sub in analysis.subsystem_health[:5]: 

157 trend_symbol = {"improving": "↓", "degrading": "↑", "stable": "→"}.get(sub.trend, "?") 

158 lines.append( 

159 f" {sub.subsystem:30}: {sub.total_issues:3} total, " 

160 f"{sub.recent_issues:2} recent {trend_symbol}" 

161 ) 

162 

163 # Hotspot analysis 

164 if analysis.hotspot_analysis: 

165 hotspots = analysis.hotspot_analysis 

166 

167 if hotspots.file_hotspots: 

168 lines.append("") 

169 lines.append("File Hotspots") 

170 lines.append("-" * 13) 

171 for h in hotspots.file_hotspots[:5]: 

172 types_str = ", ".join(f"{k}:{v}" for k, v in sorted(h.issue_types.items())) 

173 churn_flag = " [HIGH CHURN]" if h.churn_indicator == "high" else "" 

174 lines.append(f" {h.path:40}: {h.issue_count:2} issues ({types_str}){churn_flag}") 

175 

176 if hotspots.bug_magnets: 

177 lines.append("") 

178 lines.append("Bug Magnets (>60% bugs)") 

179 lines.append("-" * 23) 

180 for h in hotspots.bug_magnets: 

181 lines.append( 

182 f" {h.path}: {h.bug_ratio * 100:.0f}% bugs " 

183 f"({h.issue_types.get('BUG', 0)}/{h.issue_count})" 

184 ) 

185 

186 # Coupling analysis 

187 if analysis.coupling_analysis: 

188 coupling = analysis.coupling_analysis 

189 

190 if coupling.pairs: 

191 lines.append("") 

192 lines.append("Coupling Detection") 

193 lines.append("-" * 18) 

194 

195 lines.append("Highly Coupled File Pairs:") 

196 for i, p in enumerate(coupling.pairs[:5], 1): 

197 strength_label = ( 

198 "HIGH" 

199 if p.coupling_strength >= 0.7 

200 else "MEDIUM" 

201 if p.coupling_strength >= 0.5 

202 else "LOW" 

203 ) 

204 lines.append(f" {i}. {p.file_a} <-> {p.file_b}") 

205 lines.append( 

206 f" Co-occurrences: {p.co_occurrence_count}, " 

207 f"Strength: {p.coupling_strength:.2f} [{strength_label}]" 

208 ) 

209 

210 if coupling.clusters: 

211 lines.append("") 

212 lines.append("Coupling Clusters:") 

213 for i, cluster in enumerate(coupling.clusters[:3], 1): 

214 files_str = ", ".join(cluster[:4]) 

215 if len(cluster) > 4: 

216 files_str += f" (+{len(cluster) - 4} more)" 

217 lines.append(f" {i}. [{files_str}]") 

218 

219 if coupling.hotspots: 

220 lines.append("") 

221 lines.append("Coupling Hotspots (coupled with 3+ files):") 

222 for f in coupling.hotspots[:5]: 

223 lines.append(f" - {f}") 

224 

225 # Regression clustering analysis 

226 if analysis.regression_analysis: 

227 regression = analysis.regression_analysis 

228 

229 if regression.clusters: 

230 lines.append("") 

231 lines.append("Regression Clustering") 

232 lines.append("-" * 20) 

233 lines.append(f"Total regression chains detected: {regression.total_regression_chains}") 

234 lines.append("") 

235 lines.append("Fragile Code Clusters:") 

236 for i, c in enumerate(regression.clusters[:5], 1): 

237 severity_flag = ( 

238 f" [{c.severity.upper()}]" if c.severity in ("critical", "high") else "" 

239 ) 

240 lines.append(f" {i}. {c.primary_file}{severity_flag}") 

241 lines.append(f" Regression count: {c.regression_count}") 

242 lines.append(f" Pattern: {c.time_pattern}") 

243 if c.fix_bug_pairs: 

244 chain = " -> ".join(f"{a} fix -> {b}" for a, b in c.fix_bug_pairs[:3]) 

245 if len(c.fix_bug_pairs) > 3: 

246 chain += " ..." 

247 lines.append(f" Chain: {chain}") 

248 

249 # Test gap analysis 

250 if analysis.test_gap_analysis: 

251 tga = analysis.test_gap_analysis 

252 

253 if tga.gaps: 

254 lines.append("") 

255 lines.append("Test Gap Correlation") 

256 lines.append("-" * 20) 

257 

258 # Show correlation stats 

259 lines.append(f" Files with tests: avg {tga.files_with_tests_avg_bugs:.1f} bugs") 

260 lines.append(f" Files without tests: avg {tga.files_without_tests_avg_bugs:.1f} bugs") 

261 lines.append("") 

262 

263 # Show critical gaps 

264 critical_gaps = [g for g in tga.gaps if g.priority in ("critical", "high")] 

265 if critical_gaps: 

266 lines.append("Critical Test Gaps:") 

267 for g in critical_gaps[:5]: 

268 test_status = "NO TEST" if not g.has_test_file else g.test_file_path 

269 lines.append(f" {g.source_file} [{g.priority.upper()}]") 

270 bug_ids_str = ", ".join(g.bug_ids[:3]) 

271 lines.append(f" Bugs: {g.bug_count} ({bug_ids_str})") 

272 lines.append(f" Test: {test_status}") 

273 

274 if tga.priority_test_targets: 

275 lines.append("") 

276 lines.append("Priority Test Targets:") 

277 for i, target in enumerate(tga.priority_test_targets[:5], 1): 

278 lines.append(f" {i}. {target}") 

279 

280 # Rejection analysis 

281 if analysis.rejection_analysis: 

282 rej = analysis.rejection_analysis 

283 overall = rej.overall 

284 

285 if overall.total_closed > 0: 

286 lines.append("") 

287 lines.append("Rejection Analysis") 

288 lines.append("-" * 18) 

289 lines.append( 

290 f" Overall rejection rate: {overall.rejection_rate * 100:.1f}% " 

291 f"({overall.rejected_count}/{overall.total_closed})" 

292 ) 

293 lines.append( 

294 f" Invalid rate: {overall.invalid_rate * 100:.1f}% " 

295 f"({overall.invalid_count}/{overall.total_closed})" 

296 ) 

297 if overall.duplicate_count > 0: 

298 lines.append(f" Duplicates: {overall.duplicate_count}") 

299 if overall.deferred_count > 0: 

300 lines.append(f" Deferred: {overall.deferred_count}") 

301 

302 # By type 

303 if rej.by_type: 

304 lines.append("") 

305 lines.append(" By Type:") 

306 for issue_type in sorted(rej.by_type.keys()): 

307 metrics = rej.by_type[issue_type] 

308 rate = metrics.rejection_rate + metrics.invalid_rate 

309 lines.append(f" {issue_type:5}: {rate * 100:.1f}% non-completion") 

310 

311 # Trend 

312 if rej.by_month: 

313 sorted_months = sorted(rej.by_month.keys())[-6:] 

314 if len(sorted_months) >= 2: 

315 lines.append("") 

316 lines.append(" Trend (last 6 months):") 

317 trend_parts = [] 

318 for month in sorted_months: 

319 m = rej.by_month[month] 

320 rate = (m.rejection_rate + m.invalid_rate) * 100 

321 trend_parts.append(f"{month[-2:]}: {rate:.0f}%") 

322 lines.append(f" {', '.join(trend_parts)}") 

323 trend_symbol = {"improving": "↓", "degrading": "↑", "stable": "→"}.get( 

324 rej.trend, "→" 

325 ) 

326 lines.append(f" Direction: {rej.trend} {trend_symbol}") 

327 

328 # Common reasons 

329 if rej.common_reasons: 

330 lines.append("") 

331 lines.append(" Common Rejection Reasons:") 

332 for reason, count in rej.common_reasons[:5]: 

333 lines.append(f' - "{reason}" ({count})') 

334 

335 # Manual pattern analysis 

336 if analysis.manual_pattern_analysis: 

337 mpa = analysis.manual_pattern_analysis 

338 

339 if mpa.patterns: 

340 lines.append("") 

341 lines.append("Manual Pattern Analysis") 

342 lines.append("-" * 23) 

343 lines.append(f" Total manual interventions: {mpa.total_manual_interventions}") 

344 lines.append( 

345 f" Potentially automatable: {mpa.automatable_percentage:.0f}% " 

346 f"({mpa.automatable_count}/{mpa.total_manual_interventions})" 

347 ) 

348 lines.append("") 

349 lines.append(" Recurring Patterns:") 

350 

351 for i, pattern in enumerate(mpa.patterns[:5], 1): 

352 lines.append("") 

353 lines.append( 

354 f" {i}. {pattern.pattern_description} ({pattern.occurrence_count} occurrences)" 

355 ) 

356 issues_str = ", ".join(pattern.affected_issues[:3]) 

357 if len(pattern.affected_issues) > 3: 

358 issues_str += ", ..." 

359 lines.append(f" Issues: {issues_str}") 

360 lines.append(f" Suggestion: {pattern.suggested_automation}") 

361 lines.append(f" Complexity: {pattern.automation_complexity}") 

362 

363 # Configuration gaps analysis 

364 if analysis.config_gaps_analysis: 

365 cga = analysis.config_gaps_analysis 

366 

367 lines.append("") 

368 lines.append("Configuration Gaps Analysis") 

369 lines.append("-" * 27) 

370 lines.append(f" Coverage score: {cga.coverage_score * 100:.0f}%") 

371 lines.append(f" Current hooks: {', '.join(cga.current_hooks) or 'none'}") 

372 lines.append(f" Current skills: {len(cga.current_skills)}") 

373 lines.append(f" Current agents: {len(cga.current_agents)}") 

374 

375 if cga.gaps: 

376 lines.append("") 

377 lines.append(" Identified Gaps:") 

378 

379 for i, gap in enumerate(cga.gaps[:5], 1): 

380 lines.append("") 

381 lines.append(f" {i}. Missing: {gap.gap_type} for {gap.description}") 

382 lines.append(f" Priority: {gap.priority}") 

383 issues_str = ", ".join(gap.evidence[:3]) 

384 if len(gap.evidence) > 3: 

385 issues_str += ", ..." 

386 lines.append(f" Evidence: {issues_str}") 

387 if gap.suggested_config: 

388 lines.append(" Suggested config:") 

389 for config_line in gap.suggested_config.split("\n")[:4]: 

390 lines.append(f" {config_line}") 

391 

392 # Agent effectiveness analysis 

393 if analysis.agent_effectiveness_analysis: 

394 aea = analysis.agent_effectiveness_analysis 

395 

396 if aea.outcomes: 

397 lines.append("") 

398 lines.append("Agent Effectiveness Analysis") 

399 lines.append("-" * 28) 

400 

401 # Group by agent 

402 by_agent: dict[str, list[AgentOutcome]] = {} 

403 for outcome in aea.outcomes: 

404 if outcome.agent_name not in by_agent: 

405 by_agent[outcome.agent_name] = [] 

406 by_agent[outcome.agent_name].append(outcome) 

407 

408 for agent in sorted(by_agent.keys()): 

409 lines.append(f" {agent}:") 

410 for outcome in sorted(by_agent[agent], key=lambda o: o.issue_type): 

411 rate_pct = outcome.success_rate * 100 

412 flag = " [!]" if outcome.total_count >= 5 and rate_pct < 50 else "" 

413 lines.append( 

414 f" {outcome.issue_type:5}: {rate_pct:5.1f}% success " 

415 f"({outcome.success_count}/{outcome.total_count}){flag}" 

416 ) 

417 

418 # Recommendations 

419 if aea.best_agent_by_type or aea.problematic_combinations: 

420 lines.append("") 

421 lines.append(" Recommendations:") 

422 for issue_type, best_agent in sorted(aea.best_agent_by_type.items()): 

423 lines.append(f" - {issue_type}: best handled by {best_agent}") 

424 for agent, issue_type, reason in aea.problematic_combinations[:3]: 

425 lines.append(f" - {agent} underperforms for {issue_type} ({reason})") 

426 

427 # Complexity proxy analysis 

428 if analysis.complexity_proxy_analysis: 

429 cpa = analysis.complexity_proxy_analysis 

430 

431 lines.append("") 

432 lines.append("Complexity Proxy Analysis") 

433 lines.append("-" * 25) 

434 lines.append(f" Baseline resolution time: {cpa.baseline_days:.1f} days (median)") 

435 

436 if cpa.file_complexity: 

437 lines.append("") 

438 lines.append(" High Complexity Files (by resolution time):") 

439 for i, cp in enumerate(cpa.file_complexity[:5], 1): 

440 score_label = ( 

441 "HIGH" 

442 if cp.complexity_score >= 0.7 

443 else "MEDIUM" 

444 if cp.complexity_score >= 0.4 

445 else "LOW" 

446 ) 

447 lines.append(f" {i}. {cp.path}") 

448 lines.append( 

449 f" Avg: {cp.avg_resolution_days:.1f} days ({cp.comparison_to_baseline})" 

450 ) 

451 lines.append( 

452 f" Median: {cp.median_resolution_days:.1f} days, Issues: {cp.issue_count}" 

453 ) 

454 lines.append( 

455 f" Slowest: {cp.slowest_issue[0]} ({cp.slowest_issue[1]:.1f} days)" 

456 ) 

457 lines.append(f" Complexity score: {cp.complexity_score:.2f} [{score_label}]") 

458 

459 if cpa.directory_complexity: 

460 lines.append("") 

461 lines.append(" High Complexity Directories:") 

462 for cp in cpa.directory_complexity[:5]: 

463 lines.append( 

464 f" {cp.path}: avg {cp.avg_resolution_days:.1f} days ({cp.comparison_to_baseline})" 

465 ) 

466 

467 if cpa.complexity_outliers: 

468 lines.append("") 

469 lines.append(" Complexity Outliers (>2x baseline):") 

470 for path in cpa.complexity_outliers[:5]: 

471 lines.append(f" - {path}") 

472 

473 # Cross-cutting concern analysis 

474 if analysis.cross_cutting_analysis: 

475 cca = analysis.cross_cutting_analysis 

476 

477 if cca.smells: 

478 lines.append("") 

479 lines.append("Cross-Cutting Concern Analysis") 

480 lines.append("-" * 30) 

481 

482 for i, smell in enumerate(cca.smells[:5], 1): 

483 scatter_label = ( 

484 "HIGH" 

485 if smell.scatter_score >= 0.6 

486 else "MEDIUM" 

487 if smell.scatter_score >= 0.3 

488 else "LOW" 

489 ) 

490 lines.append("") 

491 lines.append(f" {i}. {smell.concern_type.title()} [{scatter_label} SCATTER]") 

492 dirs_str = ", ".join(smell.affected_directories[:3]) 

493 if len(smell.affected_directories) > 3: 

494 dirs_str += ", ..." 

495 lines.append(f" Directories: {dirs_str}") 

496 issues_str = ", ".join(smell.issue_ids[:3]) 

497 if len(smell.issue_ids) > 3: 

498 issues_str += ", ..." 

499 lines.append(f" Issues: {issues_str} ({smell.issue_count} total)") 

500 lines.append(f" Scatter score: {smell.scatter_score:.2f}") 

501 lines.append(f" Suggested pattern: {smell.suggested_pattern}") 

502 

503 if cca.consolidation_opportunities: 

504 lines.append("") 

505 lines.append(" Consolidation Opportunities:") 

506 for opp in cca.consolidation_opportunities[:5]: 

507 lines.append(f" - {opp}") 

508 

509 # Technical debt 

510 if analysis.debt_metrics: 

511 lines.append("") 

512 lines.append("Technical Debt") 

513 lines.append("-" * 14) 

514 debt = analysis.debt_metrics 

515 lines.append(f" Backlog Size: {debt.backlog_size}") 

516 lines.append(f" Growth Rate: {debt.backlog_growth_rate:+.1f} issues/week") 

517 lines.append(f" High Priority Open (P0-P1): {debt.high_priority_open}") 

518 lines.append(f" Aging >30 days: {debt.aging_30_plus}") 

519 

520 # Comparison 

521 if analysis.comparison_period and analysis.current_period and analysis.previous_period: 

522 lines.append("") 

523 lines.append(f"Comparison ({analysis.comparison_period})") 

524 lines.append("-" * 20) 

525 curr = analysis.current_period 

526 prev = analysis.previous_period 

527 

528 if prev.total_completed > 0: 

529 change = (curr.total_completed - prev.total_completed) / prev.total_completed * 100 

530 lines.append( 

531 f" Completed: {prev.total_completed} -> {curr.total_completed} ({change:+.0f}%)" 

532 ) 

533 else: 

534 lines.append(f" Completed: {prev.total_completed} -> {curr.total_completed}") 

535 

536 return "\n".join(lines) 

537 

538 

539def format_analysis_markdown(analysis: HistoryAnalysis) -> str: 

540 """Format analysis as Markdown report. 

541 

542 Args: 

543 analysis: HistoryAnalysis to format 

544 

545 Returns: 

546 Markdown string 

547 """ 

548 lines: list[str] = [] 

549 

550 lines.append("# Issue History Analysis Report") 

551 lines.append("") 

552 lines.append( 

553 f"**Generated**: {analysis.generated_date} | " 

554 f"**Total Completed**: {analysis.total_completed} | " 

555 f"**Active Issues**: {analysis.total_active}" 

556 ) 

557 

558 if analysis.date_range_start and analysis.date_range_end: 

559 lines.append(f"**Date Range**: {analysis.date_range_start} to {analysis.date_range_end}") 

560 

561 # Executive Summary 

562 lines.append("") 

563 lines.append("## Executive Summary") 

564 lines.append("") 

565 lines.append("| Metric | Value | Trend |") 

566 lines.append("|--------|-------|-------|") 

567 

568 velocity = f"{analysis.summary.velocity:.2f}/day" if analysis.summary.velocity else "N/A" 

569 velocity_symbol = {"increasing": "↑", "decreasing": "↓", "stable": "→"}.get( 

570 analysis.velocity_trend, "" 

571 ) 

572 lines.append(f"| Velocity | {velocity} | {velocity_symbol} {analysis.velocity_trend} |") 

573 

574 bug_count = analysis.summary.type_counts.get("BUG", 0) 

575 total = analysis.total_completed or 1 

576 bug_pct = bug_count * 100 // total 

577 bug_symbol = {"increasing": "↑ ⚠️", "decreasing": "↓ ✓", "stable": "→"}.get( 

578 analysis.bug_ratio_trend, "" 

579 ) 

580 lines.append(f"| Bug Ratio | {bug_pct}% | {bug_symbol} |") 

581 

582 if analysis.debt_metrics: 

583 growth = analysis.debt_metrics.backlog_growth_rate 

584 growth_status = "↓ ✓" if growth < 0 else ("→" if growth == 0 else "↑ ⚠️") 

585 lines.append(f"| Backlog Growth | {growth:+.1f}/week | {growth_status} |") 

586 

587 # Type Distribution 

588 lines.append("") 

589 lines.append("## Type Distribution") 

590 lines.append("") 

591 lines.append("| Type | Count | Percentage |") 

592 lines.append("|------|-------|------------|") 

593 for issue_type, count in analysis.summary.type_counts.items(): 

594 pct = count * 100 // total 

595 lines.append(f"| {issue_type} | {count} | {pct}% |") 

596 

597 # Period Trends 

598 if analysis.period_metrics: 

599 lines.append("") 

600 lines.append("## Period Trends") 

601 lines.append("") 

602 lines.append("| Period | Completed | Bug % |") 

603 lines.append("|--------|-----------|-------|") 

604 for period in analysis.period_metrics[-8:]: # Last 8 

605 bug_pct_str = f"{period.bug_ratio * 100:.0f}%" if period.bug_ratio else "N/A" 

606 lines.append(f"| {period.period_label} | {period.total_completed} | {bug_pct_str} |") 

607 

608 # Subsystem Health 

609 if analysis.subsystem_health: 

610 lines.append("") 

611 lines.append("## Subsystem Health") 

612 lines.append("") 

613 lines.append("| Subsystem | Total | Recent (30d) | Trend |") 

614 lines.append("|-----------|-------|--------------|-------|") 

615 for sub in analysis.subsystem_health: 

616 trend_symbol = {"improving": "↓ ✓", "degrading": "↑ ⚠️", "stable": "→"}.get( 

617 sub.trend, "" 

618 ) 

619 lines.append( 

620 f"| `{sub.subsystem}` | {sub.total_issues} | {sub.recent_issues} | {trend_symbol} |" 

621 ) 

622 

623 # Hotspot Analysis 

624 if analysis.hotspot_analysis: 

625 hotspots = analysis.hotspot_analysis 

626 

627 if hotspots.file_hotspots: 

628 lines.append("") 

629 lines.append("## File Hotspots") 

630 lines.append("") 

631 lines.append("| File | Issues | Types | Churn |") 

632 lines.append("|------|--------|-------|-------|") 

633 for h in hotspots.file_hotspots: 

634 types_str = ", ".join(f"{k}:{v}" for k, v in sorted(h.issue_types.items())) 

635 churn_badge = ( 

636 "🔥" 

637 if h.churn_indicator == "high" 

638 else ("⚡" if h.churn_indicator == "medium" else "") 

639 ) 

640 lines.append(f"| `{h.path}` | {h.issue_count} | {types_str} | {churn_badge} |") 

641 

642 if hotspots.directory_hotspots: 

643 lines.append("") 

644 lines.append("## Directory Hotspots") 

645 lines.append("") 

646 lines.append("| Directory | Issues | Types |") 

647 lines.append("|-----------|--------|-------|") 

648 for h in hotspots.directory_hotspots[:5]: 

649 types_str = ", ".join(f"{k}:{v}" for k, v in sorted(h.issue_types.items())) 

650 lines.append(f"| `{h.path}` | {h.issue_count} | {types_str} |") 

651 

652 if hotspots.bug_magnets: 

653 lines.append("") 

654 lines.append("## Bug Magnets") 

655 lines.append("") 

656 lines.append("Files with >60% bug ratio that may need refactoring attention:") 

657 lines.append("") 

658 lines.append("| File | Bug Ratio | Bugs/Total |") 

659 lines.append("|------|-----------|------------|") 

660 for h in hotspots.bug_magnets: 

661 lines.append( 

662 f"| `{h.path}` | {h.bug_ratio * 100:.0f}% | " 

663 f"{h.issue_types.get('BUG', 0)}/{h.issue_count} |" 

664 ) 

665 

666 # Coupling Analysis 

667 if analysis.coupling_analysis: 

668 coupling = analysis.coupling_analysis 

669 

670 if coupling.pairs: 

671 lines.append("") 

672 lines.append("## Coupling Detection") 

673 lines.append("") 

674 lines.append("Files that frequently change together across issues:") 

675 lines.append("") 

676 lines.append("| File A | File B | Co-occurrences | Strength |") 

677 lines.append("|--------|--------|----------------|----------|") 

678 for p in coupling.pairs[:10]: 

679 strength_badge = ( 

680 "🔴" 

681 if p.coupling_strength >= 0.7 

682 else ("🟠" if p.coupling_strength >= 0.5 else "🟡") 

683 ) 

684 lines.append( 

685 f"| `{p.file_a}` | `{p.file_b}` | {p.co_occurrence_count} | " 

686 f"{p.coupling_strength:.2f} {strength_badge} |" 

687 ) 

688 

689 if coupling.clusters: 

690 lines.append("") 

691 lines.append("### Coupling Clusters") 

692 lines.append("") 

693 lines.append("Groups of tightly coupled files (consider consolidating):") 

694 lines.append("") 

695 for i, cluster in enumerate(coupling.clusters[:5], 1): 

696 files_str = ", ".join(f"`{f}`" for f in cluster[:5]) 

697 if len(cluster) > 5: 

698 files_str += f" (+{len(cluster) - 5} more)" 

699 lines.append(f"{i}. {files_str}") 

700 

701 if coupling.hotspots: 

702 lines.append("") 

703 lines.append("### Coupling Hotspots") 

704 lines.append("") 

705 lines.append("Files coupled with 3+ other files (potential abstraction candidates):") 

706 lines.append("") 

707 for f in coupling.hotspots[:5]: 

708 lines.append(f"- `{f}`") 

709 

710 # Regression Clustering Analysis 

711 if analysis.regression_analysis: 

712 regression = analysis.regression_analysis 

713 

714 if regression.clusters: 

715 lines.append("") 

716 lines.append("## Regression Clustering") 

717 lines.append("") 

718 lines.append( 

719 f"**Total regression chains detected**: {regression.total_regression_chains}" 

720 ) 

721 lines.append("") 

722 lines.append("Files where fixes frequently lead to new bugs:") 

723 lines.append("") 

724 lines.append("| File | Regressions | Pattern | Severity |") 

725 lines.append("|------|-------------|---------|----------|") 

726 for c in regression.clusters: 

727 severity_badge = ( 

728 "🔴" if c.severity == "critical" else ("🟠" if c.severity == "high" else "🟡") 

729 ) 

730 lines.append( 

731 f"| `{c.primary_file}` | {c.regression_count} | " 

732 f"{c.time_pattern} | {severity_badge} |" 

733 ) 

734 

735 if regression.most_fragile_files: 

736 lines.append("") 

737 lines.append("### Most Fragile Files") 

738 lines.append("") 

739 lines.append("Files requiring architectural attention:") 

740 lines.append("") 

741 for f in regression.most_fragile_files: 

742 lines.append(f"- `{f}`") 

743 

744 # Test Gap Analysis 

745 if analysis.test_gap_analysis: 

746 tga = analysis.test_gap_analysis 

747 

748 if tga.gaps: 

749 lines.append("") 

750 lines.append("## Test Gap Correlation") 

751 lines.append("") 

752 lines.append("Correlating bug occurrences with test coverage gaps:") 

753 lines.append("") 

754 lines.append("| Metric | Value |") 

755 lines.append("|--------|-------|") 

756 lines.append(f"| Files with tests | avg {tga.files_with_tests_avg_bugs:.1f} bugs |") 

757 lines.append( 

758 f"| Files without tests | avg {tga.files_without_tests_avg_bugs:.1f} bugs |" 

759 ) 

760 lines.append("") 

761 

762 # Critical gaps table 

763 critical_gaps = [g for g in tga.gaps if g.priority in ("critical", "high")] 

764 if critical_gaps: 

765 lines.append("### Critical Test Gaps") 

766 lines.append("") 

767 lines.append("Files with high bug counts but missing tests:") 

768 lines.append("") 

769 lines.append("| File | Bugs | Priority | Test Status | Action |") 

770 lines.append("|------|------|----------|-------------|--------|") 

771 for g in critical_gaps[:10]: 

772 priority_badge = "🔴" if g.priority == "critical" else "🟠" 

773 test_status = f"`{g.test_file_path}`" if g.has_test_file else "NONE" 

774 action = "Review coverage" if g.has_test_file else "Create test file" 

775 lines.append( 

776 f"| `{g.source_file}` | {g.bug_count} | {priority_badge} | " 

777 f"{test_status} | {action} |" 

778 ) 

779 

780 if tga.priority_test_targets: 

781 lines.append("") 

782 lines.append("### Priority Test Targets") 

783 lines.append("") 

784 lines.append("Files recommended for new test creation (ordered by bug count):") 

785 lines.append("") 

786 for target in tga.priority_test_targets[:10]: 

787 lines.append(f"- `{target}`") 

788 

789 # Rejection Analysis 

790 if analysis.rejection_analysis: 

791 rej = analysis.rejection_analysis 

792 overall = rej.overall 

793 

794 if overall.total_closed > 0: 

795 lines.append("") 

796 lines.append("## Rejection Analysis") 

797 lines.append("") 

798 lines.append( 

799 f"**Overall rejection rate**: {overall.rejection_rate * 100:.1f}% " 

800 f"({overall.rejected_count}/{overall.total_closed})" 

801 ) 

802 lines.append( 

803 f"**Invalid rate**: {overall.invalid_rate * 100:.1f}% " 

804 f"({overall.invalid_count}/{overall.total_closed})" 

805 ) 

806 lines.append("") 

807 

808 # By type table 

809 if rej.by_type: 

810 lines.append("### By Issue Type") 

811 lines.append("") 

812 lines.append("| Type | Rejected | Invalid | Total | Rate |") 

813 lines.append("|------|----------|---------|-------|------|") 

814 for issue_type in sorted(rej.by_type.keys()): 

815 m = rej.by_type[issue_type] 

816 rate = (m.rejection_rate + m.invalid_rate) * 100 

817 lines.append( 

818 f"| {issue_type} | {m.rejected_count} | {m.invalid_count} | " 

819 f"{m.total_closed} | {rate:.1f}% |" 

820 ) 

821 lines.append("") 

822 

823 # Trend 

824 if rej.by_month and len(rej.by_month) >= 2: 

825 lines.append("### Trend") 

826 lines.append("") 

827 sorted_months = sorted(rej.by_month.keys())[-6:] 

828 trend_parts = [] 

829 for month in sorted_months: 

830 m = rej.by_month[month] 

831 rate = (m.rejection_rate + m.invalid_rate) * 100 

832 trend_parts.append(f"{month}: {rate:.0f}%") 

833 lines.append(" → ".join(trend_parts)) 

834 lines.append(f"*Trend: {rej.trend}*") 

835 lines.append("") 

836 

837 # Common reasons 

838 if rej.common_reasons: 

839 lines.append("### Common Rejection Reasons") 

840 lines.append("") 

841 for reason, count in rej.common_reasons[:5]: 

842 lines.append(f'- "{reason}" ({count})') 

843 

844 # Manual Pattern Analysis 

845 if analysis.manual_pattern_analysis: 

846 mpa = analysis.manual_pattern_analysis 

847 

848 if mpa.patterns: 

849 lines.append("") 

850 lines.append("## Manual Pattern Analysis") 

851 lines.append("") 

852 lines.append( 

853 f"**Total manual interventions detected**: {mpa.total_manual_interventions}" 

854 ) 

855 lines.append( 

856 f"**Potentially automatable**: {mpa.automatable_percentage:.0f}% " 

857 f"({mpa.automatable_count}/{mpa.total_manual_interventions})" 

858 ) 

859 lines.append("") 

860 lines.append("### Recurring Patterns") 

861 lines.append("") 

862 lines.append("| Pattern | Occurrences | Affected Issues | Suggestion | Complexity |") 

863 lines.append("|---------|-------------|-----------------|------------|------------|") 

864 

865 for pattern in mpa.patterns[:10]: 

866 issues_str = ", ".join(pattern.affected_issues[:3]) 

867 if len(pattern.affected_issues) > 3: 

868 issues_str += "..." 

869 lines.append( 

870 f"| {pattern.pattern_description} | {pattern.occurrence_count} | " 

871 f"{issues_str} | {pattern.suggested_automation} | " 

872 f"{pattern.automation_complexity} |" 

873 ) 

874 

875 if mpa.automation_suggestions: 

876 lines.append("") 

877 lines.append("### Automation Suggestions") 

878 lines.append("") 

879 lines.append("Based on detected patterns, consider implementing:") 

880 lines.append("") 

881 for suggestion in mpa.automation_suggestions[:5]: 

882 lines.append(f"- {suggestion}") 

883 

884 # Configuration Gaps Analysis 

885 if analysis.config_gaps_analysis: 

886 cga = analysis.config_gaps_analysis 

887 

888 lines.append("") 

889 lines.append("## Configuration Gaps Analysis") 

890 lines.append("") 

891 lines.append(f"**Coverage score**: {cga.coverage_score * 100:.0f}%") 

892 lines.append("") 

893 lines.append("### Current Configuration") 

894 lines.append("") 

895 lines.append(f"- **Hooks**: {', '.join(cga.current_hooks) or 'none'}") 

896 lines.append(f"- **Skills**: {len(cga.current_skills)}") 

897 lines.append(f"- **Agents**: {len(cga.current_agents)}") 

898 

899 if cga.gaps: 

900 lines.append("") 

901 lines.append("### Identified Gaps") 

902 lines.append("") 

903 lines.append("| Priority | Type | Description | Evidence |") 

904 lines.append("|----------|------|-------------|----------|") 

905 

906 for gap in cga.gaps[:10]: 

907 issues_str = ", ".join(gap.evidence[:3]) 

908 if len(gap.evidence) > 3: 

909 issues_str += "..." 

910 lines.append( 

911 f"| {gap.priority} | {gap.gap_type} | {gap.description} | {issues_str} |" 

912 ) 

913 

914 lines.append("") 

915 lines.append("### Suggested Configurations") 

916 lines.append("") 

917 for i, gap in enumerate(cga.gaps[:5], 1): 

918 if gap.suggested_config: 

919 lines.append(f"**{i}. {gap.description}**") 

920 lines.append("") 

921 lines.append("```json") 

922 lines.append(gap.suggested_config) 

923 lines.append("```") 

924 lines.append("") 

925 

926 # Agent Effectiveness Analysis 

927 if analysis.agent_effectiveness_analysis: 

928 aea = analysis.agent_effectiveness_analysis 

929 

930 if aea.outcomes: 

931 lines.append("") 

932 lines.append("## Agent Effectiveness Analysis") 

933 lines.append("") 

934 lines.append("| Agent | Type | Success Rate | Completed | Rejected | Failed |") 

935 lines.append("|-------|------|--------------|-----------|----------|--------|") 

936 

937 for outcome in sorted(aea.outcomes, key=lambda o: (o.agent_name, o.issue_type)): 

938 rate_pct = outcome.success_rate * 100 

939 flag = " ⚠️" if outcome.total_count >= 5 and rate_pct < 50 else "" 

940 lines.append( 

941 f"| {outcome.agent_name} | {outcome.issue_type} | " 

942 f"{rate_pct:.1f}%{flag} | {outcome.success_count} | " 

943 f"{outcome.rejection_count} | {outcome.failure_count} |" 

944 ) 

945 

946 # Recommendations 

947 if aea.best_agent_by_type or aea.problematic_combinations: 

948 lines.append("") 

949 lines.append("### Recommendations") 

950 lines.append("") 

951 for issue_type, best_agent in sorted(aea.best_agent_by_type.items()): 

952 lines.append(f"- **{issue_type}**: Best handled by `{best_agent}`") 

953 for agent, issue_type, reason in aea.problematic_combinations[:3]: 

954 lines.append(f"- **{agent}** underperforms for {issue_type} ({reason})") 

955 

956 # Technical Debt 

957 if analysis.debt_metrics: 

958 lines.append("") 

959 lines.append("## Technical Debt Health") 

960 lines.append("") 

961 debt = analysis.debt_metrics 

962 lines.append("| Metric | Value | Assessment |") 

963 lines.append("|--------|-------|------------|") 

964 

965 backlog_status = ( 

966 "✓ Low" 

967 if debt.backlog_size < 20 

968 else ("⚠️ High" if debt.backlog_size > 50 else "Moderate") 

969 ) 

970 lines.append(f"| Backlog Size | {debt.backlog_size} | {backlog_status} |") 

971 

972 growth_status = ( 

973 "✓ Shrinking" 

974 if debt.backlog_growth_rate < 0 

975 else ("⚠️ Growing" if debt.backlog_growth_rate > 2 else "Stable") 

976 ) 

977 lines.append(f"| Growth Rate | {debt.backlog_growth_rate:+.1f}/week | {growth_status} |") 

978 

979 hp_status = "✓ Good" if debt.high_priority_open < 3 else "⚠️ Attention needed" 

980 lines.append(f"| High Priority Open | {debt.high_priority_open} | {hp_status} |") 

981 

982 aging_status = ( 

983 "✓ Healthy" 

984 if debt.aging_30_plus < 5 

985 else ("⚠️ Review needed" if debt.aging_30_plus > 10 else "Moderate") 

986 ) 

987 lines.append(f"| Aging >30 days | {debt.aging_30_plus} | {aging_status} |") 

988 

989 # Comparison 

990 if analysis.comparison_period and analysis.current_period and analysis.previous_period: 

991 lines.append("") 

992 lines.append(f"## Comparative Analysis (Last {analysis.comparison_period})") 

993 lines.append("") 

994 curr = analysis.current_period 

995 prev = analysis.previous_period 

996 

997 lines.append("| Metric | Previous | Current | Change |") 

998 lines.append("|--------|----------|---------|--------|") 

999 

1000 if prev.total_completed > 0: 

1001 change = (curr.total_completed - prev.total_completed) / prev.total_completed * 100 

1002 change_str = f"{change:+.0f}%" 

1003 else: 

1004 change_str = "N/A" 

1005 lines.append( 

1006 f"| Completed | {prev.total_completed} | {curr.total_completed} | {change_str} |" 

1007 ) 

1008 

1009 prev_bugs = prev.type_counts.get("BUG", 0) 

1010 curr_bugs = curr.type_counts.get("BUG", 0) 

1011 if prev_bugs > 0: 

1012 bug_change = (curr_bugs - prev_bugs) / prev_bugs * 100 

1013 bug_change_str = f"{bug_change:+.0f}%" 

1014 if bug_change < 0: 

1015 bug_change_str += " ✓" 

1016 else: 

1017 bug_change_str = "N/A" 

1018 lines.append(f"| Bugs Fixed | {prev_bugs} | {curr_bugs} | {bug_change_str} |") 

1019 

1020 return "\n".join(lines)