Coverage for little_loops / issue_history / formatting.py: 0%
659 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
1"""Issue history formatting functions.
3Provides functions to format issue history summaries and analyses
4as text, JSON, YAML, and Markdown.
5"""
7from __future__ import annotations
9import json
11from little_loops.issue_history.models import (
12 AgentOutcome,
13 HistoryAnalysis,
14 HistorySummary,
15)
18def format_summary_text(summary: HistorySummary) -> str:
19 """Format summary as human-readable text.
21 Args:
22 summary: HistorySummary to format
24 Returns:
25 Formatted text string
26 """
27 lines: list[str] = []
29 lines.append("Issue History Summary")
30 lines.append("=" * 21)
31 lines.append(f"Total Completed: {summary.total_count}")
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")
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}%)")
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}%)")
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}%)")
58 return "\n".join(lines)
61def format_summary_json(summary: HistorySummary) -> str:
62 """Format summary as JSON.
64 Args:
65 summary: HistorySummary to format
67 Returns:
68 JSON string
69 """
70 return json.dumps(summary.to_dict(), indent=2)
73def format_analysis_json(analysis: HistoryAnalysis) -> str:
74 """Format analysis as JSON.
76 Args:
77 analysis: HistoryAnalysis to format
79 Returns:
80 JSON string
81 """
82 return json.dumps(analysis.to_dict(), indent=2)
85def format_analysis_yaml(analysis: HistoryAnalysis) -> str:
86 """Format analysis as YAML.
88 Args:
89 analysis: HistoryAnalysis to format
91 Returns:
92 YAML string (falls back to JSON if yaml not available)
93 """
94 try:
95 import yaml
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)
103def format_analysis_text(analysis: HistoryAnalysis) -> str:
104 """Format analysis as human-readable text.
106 Args:
107 analysis: HistoryAnalysis to format
109 Returns:
110 Formatted text string
111 """
112 lines: list[str] = []
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}")
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}")
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}")
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}%)")
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 )
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 )
163 # Hotspot analysis
164 if analysis.hotspot_analysis:
165 hotspots = analysis.hotspot_analysis
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}")
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 )
186 # Coupling analysis
187 if analysis.coupling_analysis:
188 coupling = analysis.coupling_analysis
190 if coupling.pairs:
191 lines.append("")
192 lines.append("Coupling Detection")
193 lines.append("-" * 18)
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 )
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}]")
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}")
225 # Regression clustering analysis
226 if analysis.regression_analysis:
227 regression = analysis.regression_analysis
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}")
249 # Test gap analysis
250 if analysis.test_gap_analysis:
251 tga = analysis.test_gap_analysis
253 if tga.gaps:
254 lines.append("")
255 lines.append("Test Gap Correlation")
256 lines.append("-" * 20)
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("")
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}")
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}")
280 # Rejection analysis
281 if analysis.rejection_analysis:
282 rej = analysis.rejection_analysis
283 overall = rej.overall
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}")
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")
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}")
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})')
335 # Manual pattern analysis
336 if analysis.manual_pattern_analysis:
337 mpa = analysis.manual_pattern_analysis
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:")
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}")
363 # Configuration gaps analysis
364 if analysis.config_gaps_analysis:
365 cga = analysis.config_gaps_analysis
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)}")
375 if cga.gaps:
376 lines.append("")
377 lines.append(" Identified Gaps:")
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}")
392 # Agent effectiveness analysis
393 if analysis.agent_effectiveness_analysis:
394 aea = analysis.agent_effectiveness_analysis
396 if aea.outcomes:
397 lines.append("")
398 lines.append("Agent Effectiveness Analysis")
399 lines.append("-" * 28)
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)
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 )
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})")
427 # Complexity proxy analysis
428 if analysis.complexity_proxy_analysis:
429 cpa = analysis.complexity_proxy_analysis
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)")
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}]")
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 )
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}")
473 # Cross-cutting concern analysis
474 if analysis.cross_cutting_analysis:
475 cca = analysis.cross_cutting_analysis
477 if cca.smells:
478 lines.append("")
479 lines.append("Cross-Cutting Concern Analysis")
480 lines.append("-" * 30)
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}")
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}")
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}")
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
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}")
536 return "\n".join(lines)
539def format_analysis_markdown(analysis: HistoryAnalysis) -> str:
540 """Format analysis as Markdown report.
542 Args:
543 analysis: HistoryAnalysis to format
545 Returns:
546 Markdown string
547 """
548 lines: list[str] = []
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 )
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}")
561 # Executive Summary
562 lines.append("")
563 lines.append("## Executive Summary")
564 lines.append("")
565 lines.append("| Metric | Value | Trend |")
566 lines.append("|--------|-------|-------|")
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} |")
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} |")
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} |")
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}% |")
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} |")
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 )
623 # Hotspot Analysis
624 if analysis.hotspot_analysis:
625 hotspots = analysis.hotspot_analysis
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} |")
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} |")
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 )
666 # Coupling Analysis
667 if analysis.coupling_analysis:
668 coupling = analysis.coupling_analysis
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 )
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}")
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}`")
710 # Regression Clustering Analysis
711 if analysis.regression_analysis:
712 regression = analysis.regression_analysis
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 )
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}`")
744 # Test Gap Analysis
745 if analysis.test_gap_analysis:
746 tga = analysis.test_gap_analysis
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("")
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 )
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}`")
789 # Rejection Analysis
790 if analysis.rejection_analysis:
791 rej = analysis.rejection_analysis
792 overall = rej.overall
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("")
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("")
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("")
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})')
844 # Manual Pattern Analysis
845 if analysis.manual_pattern_analysis:
846 mpa = analysis.manual_pattern_analysis
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("|---------|-------------|-----------------|------------|------------|")
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 )
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}")
884 # Configuration Gaps Analysis
885 if analysis.config_gaps_analysis:
886 cga = analysis.config_gaps_analysis
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)}")
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("|----------|------|-------------|----------|")
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 )
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("")
926 # Agent Effectiveness Analysis
927 if analysis.agent_effectiveness_analysis:
928 aea = analysis.agent_effectiveness_analysis
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("|-------|------|--------------|-----------|----------|--------|")
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 )
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})")
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("|--------|-------|------------|")
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} |")
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} |")
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} |")
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} |")
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
997 lines.append("| Metric | Previous | Current | Change |")
998 lines.append("|--------|----------|---------|--------|")
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 )
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} |")
1020 return "\n".join(lines)