Coverage for little_loops / issue_history / formatting.py: 0%
743 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-16 13:08 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-16 13:08 -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 # 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)
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)")
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}")
400 # Configuration gaps analysis
401 if analysis.config_gaps_analysis:
402 cga = analysis.config_gaps_analysis
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)}")
412 if cga.gaps:
413 lines.append("")
414 lines.append(" Identified Gaps:")
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}")
429 # Agent effectiveness analysis
430 if analysis.agent_effectiveness_analysis:
431 aea = analysis.agent_effectiveness_analysis
433 if aea.outcomes:
434 lines.append("")
435 lines.append("Agent Effectiveness Analysis")
436 lines.append("-" * 28)
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)
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 )
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})")
464 # Complexity proxy analysis
465 if analysis.complexity_proxy_analysis:
466 cpa = analysis.complexity_proxy_analysis
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)")
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}]")
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 )
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}")
510 # Cross-cutting concern analysis
511 if analysis.cross_cutting_analysis:
512 cca = analysis.cross_cutting_analysis
514 if cca.smells:
515 lines.append("")
516 lines.append("Cross-Cutting Concern Analysis")
517 lines.append("-" * 30)
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}")
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}")
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}")
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
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}")
573 return "\n".join(lines)
576def format_analysis_markdown(analysis: HistoryAnalysis) -> str:
577 """Format analysis as Markdown report.
579 Args:
580 analysis: HistoryAnalysis to format
582 Returns:
583 Markdown string
584 """
585 lines: list[str] = []
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 )
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}")
598 # Executive Summary
599 lines.append("")
600 lines.append("## Executive Summary")
601 lines.append("")
602 lines.append("| Metric | Value | Trend |")
603 lines.append("|--------|-------|-------|")
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} |")
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} |")
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} |")
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}% |")
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} |")
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 )
660 # Hotspot Analysis
661 if analysis.hotspot_analysis:
662 hotspots = analysis.hotspot_analysis
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} |")
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} |")
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 )
703 # Coupling Analysis
704 if analysis.coupling_analysis:
705 coupling = analysis.coupling_analysis
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 )
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}")
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}`")
747 # Regression Clustering Analysis
748 if analysis.regression_analysis:
749 regression = analysis.regression_analysis
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 )
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}`")
781 # Test Gap Analysis
782 if analysis.test_gap_analysis:
783 tga = analysis.test_gap_analysis
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("")
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 )
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}`")
826 # Rejection Analysis
827 if analysis.rejection_analysis:
828 rej = analysis.rejection_analysis
829 overall = rej.overall
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("")
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("")
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("")
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})')
881 # Manual Pattern Analysis
882 if analysis.manual_pattern_analysis:
883 mpa = analysis.manual_pattern_analysis
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("|---------|-------------|-----------------|------------|------------|")
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 )
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}")
921 # Evolution Triggers
922 if analysis.recurring_feedback_analysis or analysis.skill_bypass_analysis:
923 lines.append("")
924 lines.append("## Evolution Triggers")
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]}")
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}")
988 # Configuration Gaps Analysis
989 if analysis.config_gaps_analysis:
990 cga = analysis.config_gaps_analysis
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)}")
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("|----------|------|-------------|----------|")
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 )
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("")
1030 # Agent Effectiveness Analysis
1031 if analysis.agent_effectiveness_analysis:
1032 aea = analysis.agent_effectiveness_analysis
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("|-------|------|--------------|-----------|----------|--------|")
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 )
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})")
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("|--------|-------|------------|")
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} |")
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} |")
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} |")
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} |")
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
1101 lines.append("| Metric | Previous | Current | Change |")
1102 lines.append("|--------|----------|---------|--------|")
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 )
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} |")
1124 return "\n".join(lines)