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

743 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-15 17:27 -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 # Evolution trigger analysis 

364 if analysis.recurring_feedback_analysis or analysis.skill_bypass_analysis: 

365 lines.append("") 

366 lines.append("Evolution Triggers") 

367 lines.append("-" * 18) 

368 

369 if analysis.recurring_feedback_analysis: 

370 rfa = analysis.recurring_feedback_analysis 

371 if rfa.feedbacks: 

372 lines.append("") 

373 lines.append(f" Recurring Corrections (threshold: {rfa.threshold_used}x):") 

374 for i, fb in enumerate(rfa.feedbacks[:5], 1): 

375 sessions_str = ", ".join(fb.example_sessions[:3]) 

376 if len(fb.example_sessions) > 3: 

377 sessions_str += ", ..." 

378 lines.append(f" {i}. {fb.topic} ({fb.occurrence_count}x)") 

379 if sessions_str: 

380 lines.append(f" Sessions: {sessions_str}") 

381 if fb.candidate_rule: 

382 lines.append(f" Candidate rule: {fb.candidate_rule[:80]}...") 

383 if rfa.retired_count: 

384 lines.append(f" ({rfa.retired_count} cluster(s) excluded — already retired)") 

385 

386 if analysis.skill_bypass_analysis: 

387 sba = analysis.skill_bypass_analysis 

388 if sba.bypasses: 

389 lines.append("") 

390 lines.append(f" Skill Bypasses (threshold: {sba.threshold_used}x):") 

391 for i, bypass in enumerate(sba.bypasses[:5], 1): 

392 sessions_str = ", ".join(bypass.example_sessions[:3]) 

393 if len(bypass.example_sessions) > 3: 

394 sessions_str += ", ..." 

395 lines.append(f" {i}. {bypass.skill_name} ({bypass.bypass_count}x)") 

396 if sessions_str: 

397 lines.append(f" Sessions: {sessions_str}") 

398 lines.append(f" Suggestion: {bypass.suggested_improvement}") 

399 

400 # Configuration gaps analysis 

401 if analysis.config_gaps_analysis: 

402 cga = analysis.config_gaps_analysis 

403 

404 lines.append("") 

405 lines.append("Configuration Gaps Analysis") 

406 lines.append("-" * 27) 

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

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

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

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

411 

412 if cga.gaps: 

413 lines.append("") 

414 lines.append(" Identified Gaps:") 

415 

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

417 lines.append("") 

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

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

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

421 if len(gap.evidence) > 3: 

422 issues_str += ", ..." 

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

424 if gap.suggested_config: 

425 lines.append(" Suggested config:") 

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

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

428 

429 # Agent effectiveness analysis 

430 if analysis.agent_effectiveness_analysis: 

431 aea = analysis.agent_effectiveness_analysis 

432 

433 if aea.outcomes: 

434 lines.append("") 

435 lines.append("Agent Effectiveness Analysis") 

436 lines.append("-" * 28) 

437 

438 # Group by agent 

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

440 for outcome in aea.outcomes: 

441 if outcome.agent_name not in by_agent: 

442 by_agent[outcome.agent_name] = [] 

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

444 

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

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

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

448 rate_pct = outcome.success_rate * 100 

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

450 lines.append( 

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

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

453 ) 

454 

455 # Recommendations 

456 if aea.best_agent_by_type or aea.problematic_combinations: 

457 lines.append("") 

458 lines.append(" Recommendations:") 

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

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

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

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

463 

464 # Complexity proxy analysis 

465 if analysis.complexity_proxy_analysis: 

466 cpa = analysis.complexity_proxy_analysis 

467 

468 lines.append("") 

469 lines.append("Complexity Proxy Analysis") 

470 lines.append("-" * 25) 

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

472 

473 if cpa.file_complexity: 

474 lines.append("") 

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

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

477 score_label = ( 

478 "HIGH" 

479 if cp.complexity_score >= 0.7 

480 else "MEDIUM" 

481 if cp.complexity_score >= 0.4 

482 else "LOW" 

483 ) 

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

485 lines.append( 

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

487 ) 

488 lines.append( 

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

490 ) 

491 lines.append( 

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

493 ) 

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

495 

496 if cpa.directory_complexity: 

497 lines.append("") 

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

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

500 lines.append( 

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

502 ) 

503 

504 if cpa.complexity_outliers: 

505 lines.append("") 

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

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

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

509 

510 # Cross-cutting concern analysis 

511 if analysis.cross_cutting_analysis: 

512 cca = analysis.cross_cutting_analysis 

513 

514 if cca.smells: 

515 lines.append("") 

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

517 lines.append("-" * 30) 

518 

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

520 scatter_label = ( 

521 "HIGH" 

522 if smell.scatter_score >= 0.6 

523 else "MEDIUM" 

524 if smell.scatter_score >= 0.3 

525 else "LOW" 

526 ) 

527 lines.append("") 

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

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

530 if len(smell.affected_directories) > 3: 

531 dirs_str += ", ..." 

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

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

534 if len(smell.issue_ids) > 3: 

535 issues_str += ", ..." 

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

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

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

539 

540 if cca.consolidation_opportunities: 

541 lines.append("") 

542 lines.append(" Consolidation Opportunities:") 

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

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

545 

546 # Technical debt 

547 if analysis.debt_metrics: 

548 lines.append("") 

549 lines.append("Technical Debt") 

550 lines.append("-" * 14) 

551 debt = analysis.debt_metrics 

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

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

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

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

556 

557 # Comparison 

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

559 lines.append("") 

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

561 lines.append("-" * 20) 

562 curr = analysis.current_period 

563 prev = analysis.previous_period 

564 

565 if prev.total_completed > 0: 

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

567 lines.append( 

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

569 ) 

570 else: 

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

572 

573 return "\n".join(lines) 

574 

575 

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

577 """Format analysis as Markdown report. 

578 

579 Args: 

580 analysis: HistoryAnalysis to format 

581 

582 Returns: 

583 Markdown string 

584 """ 

585 lines: list[str] = [] 

586 

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

588 lines.append("") 

589 lines.append( 

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

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

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

593 ) 

594 

595 if analysis.date_range_start and analysis.date_range_end: 

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

597 

598 # Executive Summary 

599 lines.append("") 

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

601 lines.append("") 

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

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

604 

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

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

607 analysis.velocity_trend, "" 

608 ) 

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

610 

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

612 total = analysis.total_completed or 1 

613 bug_pct = bug_count * 100 // total 

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

615 analysis.bug_ratio_trend, "" 

616 ) 

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

618 

619 if analysis.debt_metrics: 

620 growth = analysis.debt_metrics.backlog_growth_rate 

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

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

623 

624 # Type Distribution 

625 lines.append("") 

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

627 lines.append("") 

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

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

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

631 pct = count * 100 // total 

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

633 

634 # Period Trends 

635 if analysis.period_metrics: 

636 lines.append("") 

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

638 lines.append("") 

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

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

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

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

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

644 

645 # Subsystem Health 

646 if analysis.subsystem_health: 

647 lines.append("") 

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

649 lines.append("") 

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

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

652 for sub in analysis.subsystem_health: 

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

654 sub.trend, "" 

655 ) 

656 lines.append( 

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

658 ) 

659 

660 # Hotspot Analysis 

661 if analysis.hotspot_analysis: 

662 hotspots = analysis.hotspot_analysis 

663 

664 if hotspots.file_hotspots: 

665 lines.append("") 

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

667 lines.append("") 

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

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

670 for h in hotspots.file_hotspots: 

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

672 churn_badge = ( 

673 "🔥" 

674 if h.churn_indicator == "high" 

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

676 ) 

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

678 

679 if hotspots.directory_hotspots: 

680 lines.append("") 

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

682 lines.append("") 

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

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

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

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

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

688 

689 if hotspots.bug_magnets: 

690 lines.append("") 

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

692 lines.append("") 

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

694 lines.append("") 

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

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

697 for h in hotspots.bug_magnets: 

698 lines.append( 

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

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

701 ) 

702 

703 # Coupling Analysis 

704 if analysis.coupling_analysis: 

705 coupling = analysis.coupling_analysis 

706 

707 if coupling.pairs: 

708 lines.append("") 

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

710 lines.append("") 

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

712 lines.append("") 

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

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

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

716 strength_badge = ( 

717 "🔴" 

718 if p.coupling_strength >= 0.7 

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

720 ) 

721 lines.append( 

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

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

724 ) 

725 

726 if coupling.clusters: 

727 lines.append("") 

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

729 lines.append("") 

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

731 lines.append("") 

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

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

734 if len(cluster) > 5: 

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

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

737 

738 if coupling.hotspots: 

739 lines.append("") 

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

741 lines.append("") 

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

743 lines.append("") 

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

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

746 

747 # Regression Clustering Analysis 

748 if analysis.regression_analysis: 

749 regression = analysis.regression_analysis 

750 

751 if regression.clusters: 

752 lines.append("") 

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

754 lines.append("") 

755 lines.append( 

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

757 ) 

758 lines.append("") 

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

760 lines.append("") 

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

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

763 for c in regression.clusters: 

764 severity_badge = ( 

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

766 ) 

767 lines.append( 

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

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

770 ) 

771 

772 if regression.most_fragile_files: 

773 lines.append("") 

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

775 lines.append("") 

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

777 lines.append("") 

778 for f in regression.most_fragile_files: 

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

780 

781 # Test Gap Analysis 

782 if analysis.test_gap_analysis: 

783 tga = analysis.test_gap_analysis 

784 

785 if tga.gaps: 

786 lines.append("") 

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

788 lines.append("") 

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

790 lines.append("") 

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

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

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

794 lines.append( 

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

796 ) 

797 lines.append("") 

798 

799 # Critical gaps table 

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

801 if critical_gaps: 

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

803 lines.append("") 

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

805 lines.append("") 

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

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

808 for g in critical_gaps[:10]: 

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

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

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

812 lines.append( 

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

814 f"{test_status} | {action} |" 

815 ) 

816 

817 if tga.priority_test_targets: 

818 lines.append("") 

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

820 lines.append("") 

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

822 lines.append("") 

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

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

825 

826 # Rejection Analysis 

827 if analysis.rejection_analysis: 

828 rej = analysis.rejection_analysis 

829 overall = rej.overall 

830 

831 if overall.total_closed > 0: 

832 lines.append("") 

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

834 lines.append("") 

835 lines.append( 

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

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

838 ) 

839 lines.append( 

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

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

842 ) 

843 lines.append("") 

844 

845 # By type table 

846 if rej.by_type: 

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

848 lines.append("") 

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

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

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

852 m = rej.by_type[issue_type] 

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

854 lines.append( 

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

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

857 ) 

858 lines.append("") 

859 

860 # Trend 

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

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

863 lines.append("") 

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

865 trend_parts = [] 

866 for month in sorted_months: 

867 m = rej.by_month[month] 

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

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

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

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

872 lines.append("") 

873 

874 # Common reasons 

875 if rej.common_reasons: 

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

877 lines.append("") 

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

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

880 

881 # Manual Pattern Analysis 

882 if analysis.manual_pattern_analysis: 

883 mpa = analysis.manual_pattern_analysis 

884 

885 if mpa.patterns: 

886 lines.append("") 

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

888 lines.append("") 

889 lines.append( 

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

891 ) 

892 lines.append( 

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

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

895 ) 

896 lines.append("") 

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

898 lines.append("") 

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

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

901 

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

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

904 if len(pattern.affected_issues) > 3: 

905 issues_str += "..." 

906 lines.append( 

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

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

909 f"{pattern.automation_complexity} |" 

910 ) 

911 

912 if mpa.automation_suggestions: 

913 lines.append("") 

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

915 lines.append("") 

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

917 lines.append("") 

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

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

920 

921 # Evolution Triggers 

922 if analysis.recurring_feedback_analysis or analysis.skill_bypass_analysis: 

923 lines.append("") 

924 lines.append("## Evolution Triggers") 

925 

926 if analysis.recurring_feedback_analysis: 

927 rfa = analysis.recurring_feedback_analysis 

928 if rfa.feedbacks: 

929 lines.append("") 

930 lines.append("### Recurring Corrections") 

931 lines.append("") 

932 lines.append( 

933 f"**Threshold**: {rfa.threshold_used}x | " 

934 f"**Total recurring corrections**: {rfa.total_recurring_corrections}" 

935 ) 

936 lines.append("") 

937 lines.append("| Topic | Count | Example Sessions | Candidate Rule |") 

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

939 for fb in rfa.feedbacks[:10]: 

940 sessions_str = ", ".join(fb.example_sessions[:2]) 

941 if len(fb.example_sessions) > 2: 

942 sessions_str += "..." 

943 rule_excerpt = fb.candidate_rule[:60] + "..." if fb.candidate_rule else "—" 

944 lines.append( 

945 f"| {fb.topic[:60]} | {fb.occurrence_count} | " 

946 f"{sessions_str} | {rule_excerpt} |" 

947 ) 

948 if rfa.retired_count: 

949 lines.append("") 

950 lines.append(f"> {rfa.retired_count} cluster(s) excluded — already retired.") 

951 if rfa.rule_candidates: 

952 lines.append("") 

953 lines.append("### Rule Candidates") 

954 lines.append("") 

955 lines.append("Proposed permanent rules based on recurrence:") 

956 lines.append("") 

957 for candidate in rfa.rule_candidates[:5]: 

958 lines.append(f"- {candidate[:120]}") 

959 

960 if analysis.skill_bypass_analysis: 

961 sba = analysis.skill_bypass_analysis 

962 if sba.bypasses: 

963 lines.append("") 

964 lines.append("### Skill Bypasses") 

965 lines.append("") 

966 lines.append( 

967 f"**Threshold**: {sba.threshold_used}x | " 

968 f"**Total bypassed invocations**: {sba.total_bypassed_invocations}" 

969 ) 

970 lines.append("") 

971 lines.append("| Skill | Bypass Count | Example Sessions | Suggested Improvement |") 

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

973 for bypass in sba.bypasses[:10]: 

974 sessions_str = ", ".join(bypass.example_sessions[:2]) 

975 if len(bypass.example_sessions) > 2: 

976 sessions_str += "..." 

977 lines.append( 

978 f"| {bypass.skill_name} | {bypass.bypass_count} | " 

979 f"{sessions_str} | {bypass.suggested_improvement[:80]} |" 

980 ) 

981 if sba.improvement_suggestions: 

982 lines.append("") 

983 lines.append("### Improvement Suggestions") 

984 lines.append("") 

985 for suggestion in sba.improvement_suggestions[:5]: 

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

987 

988 # Configuration Gaps Analysis 

989 if analysis.config_gaps_analysis: 

990 cga = analysis.config_gaps_analysis 

991 

992 lines.append("") 

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

994 lines.append("") 

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

996 lines.append("") 

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

998 lines.append("") 

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

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

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

1002 

1003 if cga.gaps: 

1004 lines.append("") 

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

1006 lines.append("") 

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

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

1009 

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

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

1012 if len(gap.evidence) > 3: 

1013 issues_str += "..." 

1014 lines.append( 

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

1016 ) 

1017 

1018 lines.append("") 

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

1020 lines.append("") 

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

1022 if gap.suggested_config: 

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

1024 lines.append("") 

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

1026 lines.append(gap.suggested_config) 

1027 lines.append("```") 

1028 lines.append("") 

1029 

1030 # Agent Effectiveness Analysis 

1031 if analysis.agent_effectiveness_analysis: 

1032 aea = analysis.agent_effectiveness_analysis 

1033 

1034 if aea.outcomes: 

1035 lines.append("") 

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

1037 lines.append("") 

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

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

1040 

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

1042 rate_pct = outcome.success_rate * 100 

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

1044 lines.append( 

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

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

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

1048 ) 

1049 

1050 # Recommendations 

1051 if aea.best_agent_by_type or aea.problematic_combinations: 

1052 lines.append("") 

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

1054 lines.append("") 

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

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

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

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

1059 

1060 # Technical Debt 

1061 if analysis.debt_metrics: 

1062 lines.append("") 

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

1064 lines.append("") 

1065 debt = analysis.debt_metrics 

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

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

1068 

1069 backlog_status = ( 

1070 "✓ Low" 

1071 if debt.backlog_size < 20 

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

1073 ) 

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

1075 

1076 growth_status = ( 

1077 "✓ Shrinking" 

1078 if debt.backlog_growth_rate < 0 

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

1080 ) 

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

1082 

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

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

1085 

1086 aging_status = ( 

1087 "✓ Healthy" 

1088 if debt.aging_30_plus < 5 

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

1090 ) 

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

1092 

1093 # Comparison 

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

1095 lines.append("") 

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

1097 lines.append("") 

1098 curr = analysis.current_period 

1099 prev = analysis.previous_period 

1100 

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

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

1103 

1104 if prev.total_completed > 0: 

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

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

1107 else: 

1108 change_str = "N/A" 

1109 lines.append( 

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

1111 ) 

1112 

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

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

1115 if prev_bugs > 0: 

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

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

1118 if bug_change < 0: 

1119 bug_change_str += " ✓" 

1120 else: 

1121 bug_change_str = "N/A" 

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

1123 

1124 return "\n".join(lines)