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

1"""Issue history formatting functions.

2

3 Provides functions to format issue history summaries and analyses

4 as text, JSON, YAML, and Markdown.

5"""

6

7 from __future__ import annotations

8

9 import json

10

11 from little_loops.issue_history.models import (

12 AgentOutcome,

13 HistoryAnalysis,

14 HistorySummary,

15)

16

17

18 def 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

61 def 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

73 def 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

85 def 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

103 def 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

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