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 # 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

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