Coverage for little_loops / issue_history / models.py: 100%
304 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
1"""Issue history data models.
3Dataclasses for issue history analysis including completed issues,
4summary statistics, hotspot detection, coupling analysis, regression
5clustering, test gap analysis, and technical debt metrics.
6"""
8from __future__ import annotations
10from dataclasses import dataclass, field
11from datetime import date, datetime
12from pathlib import Path
13from typing import Any
16@dataclass
17class CompletedIssue:
18 """Parsed information from a completed issue file."""
20 path: Path
21 issue_type: str # BUG, ENH, FEAT
22 priority: str # P0-P5
23 issue_id: str # e.g., BUG-001
24 discovered_by: str | None = None
25 discovered_date: date | None = None
26 completed_date: date | None = None
27 captured_at: datetime | None = None
28 completed_at: datetime | None = None
30 def to_dict(self) -> dict[str, Any]:
31 """Convert to dictionary for JSON serialization."""
32 return {
33 "path": str(self.path),
34 "issue_type": self.issue_type,
35 "priority": self.priority,
36 "issue_id": self.issue_id,
37 "discovered_by": self.discovered_by,
38 "discovered_date": (self.discovered_date.isoformat() if self.discovered_date else None),
39 "completed_date": (self.completed_date.isoformat() if self.completed_date else None),
40 "captured_at": (self.captured_at.isoformat() if self.captured_at else None),
41 "completed_at": (self.completed_at.isoformat() if self.completed_at else None),
42 }
45@dataclass
46class HistorySummary:
47 """Summary statistics for completed issues."""
49 total_count: int
50 type_counts: dict[str, int] = field(default_factory=dict)
51 priority_counts: dict[str, int] = field(default_factory=dict)
52 discovery_counts: dict[str, int] = field(default_factory=dict)
53 earliest_date: date | None = None
54 latest_date: date | None = None
56 @property
57 def date_range_days(self) -> int | None:
58 """Calculate days between earliest and latest completion."""
59 if self.earliest_date and self.latest_date:
60 return (self.latest_date - self.earliest_date).days + 1
61 return None
63 @property
64 def velocity(self) -> float | None:
65 """Calculate issues per day."""
66 if self.date_range_days and self.date_range_days > 0:
67 return self.total_count / self.date_range_days
68 return None
70 def to_dict(self) -> dict[str, Any]:
71 """Convert to dictionary for JSON serialization."""
72 return {
73 "total_count": self.total_count,
74 "type_counts": self.type_counts,
75 "priority_counts": self.priority_counts,
76 "discovery_counts": self.discovery_counts,
77 "earliest_date": (self.earliest_date.isoformat() if self.earliest_date else None),
78 "latest_date": self.latest_date.isoformat() if self.latest_date else None,
79 "date_range_days": self.date_range_days,
80 "velocity": round(self.velocity, 2) if self.velocity else None,
81 }
84@dataclass
85class PeriodMetrics:
86 """Metrics for a specific time period."""
88 period_start: date
89 period_end: date
90 period_label: str # e.g., "Q1 2025", "Jan 2025", "Week 3"
91 total_completed: int = 0
92 type_counts: dict[str, int] = field(default_factory=dict)
93 priority_counts: dict[str, int] = field(default_factory=dict)
94 avg_completion_days: float | None = None
96 @property
97 def bug_ratio(self) -> float | None:
98 """Calculate bug percentage."""
99 if self.total_completed == 0:
100 return None
101 bug_count = self.type_counts.get("BUG", 0)
102 return bug_count / self.total_completed
104 def to_dict(self) -> dict[str, Any]:
105 """Convert to dictionary for serialization."""
106 return {
107 "period_start": self.period_start.isoformat(),
108 "period_end": self.period_end.isoformat(),
109 "period_label": self.period_label,
110 "total_completed": self.total_completed,
111 "type_counts": self.type_counts,
112 "priority_counts": self.priority_counts,
113 "bug_ratio": round(self.bug_ratio, 3) if self.bug_ratio is not None else None,
114 "avg_completion_days": (
115 round(self.avg_completion_days, 1) if self.avg_completion_days else None
116 ),
117 }
120@dataclass
121class SubsystemHealth:
122 """Health metrics for a subsystem (directory)."""
124 subsystem: str # Directory path
125 total_issues: int = 0
126 recent_issues: int = 0 # Issues in last 30 days
127 issue_ids: list[str] = field(default_factory=list)
128 trend: str = "stable" # "improving", "stable", "degrading"
130 def to_dict(self) -> dict[str, Any]:
131 """Convert to dictionary for serialization."""
132 return {
133 "subsystem": self.subsystem,
134 "total_issues": self.total_issues,
135 "recent_issues": self.recent_issues,
136 "issue_ids": self.issue_ids[:5], # Top 5
137 "trend": self.trend,
138 }
141@dataclass
142class Hotspot:
143 """A file or directory that appears in multiple issues."""
145 path: str
146 issue_count: int = 0
147 issue_ids: list[str] = field(default_factory=list)
148 issue_types: dict[str, int] = field(default_factory=dict) # {"BUG": 5, "ENH": 3}
149 bug_ratio: float = 0.0 # bugs / total issues
150 churn_indicator: str = "low" # "high", "medium", "low"
152 def to_dict(self) -> dict[str, Any]:
153 """Convert to dictionary for serialization."""
154 return {
155 "path": self.path,
156 "issue_count": self.issue_count,
157 "issue_ids": self.issue_ids[:10], # Top 10
158 "issue_types": self.issue_types,
159 "bug_ratio": round(self.bug_ratio, 3),
160 "churn_indicator": self.churn_indicator,
161 }
164@dataclass
165class HotspotAnalysis:
166 """Analysis of files and directories appearing repeatedly in issues."""
168 file_hotspots: list[Hotspot] = field(default_factory=list)
169 directory_hotspots: list[Hotspot] = field(default_factory=list)
170 bug_magnets: list[Hotspot] = field(default_factory=list) # >60% bug ratio
172 def to_dict(self) -> dict[str, Any]:
173 """Convert to dictionary for serialization."""
174 return {
175 "file_hotspots": [h.to_dict() for h in self.file_hotspots],
176 "directory_hotspots": [h.to_dict() for h in self.directory_hotspots],
177 "bug_magnets": [h.to_dict() for h in self.bug_magnets],
178 }
181@dataclass
182class CouplingPair:
183 """A pair of files that frequently appear together in issues."""
185 file_a: str
186 file_b: str
187 co_occurrence_count: int = 0
188 coupling_strength: float = 0.0 # 0-1, Jaccard similarity
189 issue_ids: list[str] = field(default_factory=list)
191 def to_dict(self) -> dict[str, Any]:
192 """Convert to dictionary for serialization."""
193 return {
194 "file_a": self.file_a,
195 "file_b": self.file_b,
196 "co_occurrence_count": self.co_occurrence_count,
197 "coupling_strength": round(self.coupling_strength, 3),
198 "issue_ids": self.issue_ids[:10], # Top 10
199 }
202@dataclass
203class CouplingAnalysis:
204 """Analysis of files that frequently change together."""
206 pairs: list[CouplingPair] = field(default_factory=list)
207 clusters: list[list[str]] = field(default_factory=list) # Groups of coupled files
208 hotspots: list[str] = field(default_factory=list) # Files coupled with 3+ others
210 def to_dict(self) -> dict[str, Any]:
211 """Convert to dictionary for serialization."""
212 return {
213 "pairs": [p.to_dict() for p in self.pairs],
214 "clusters": self.clusters[:10], # Top 10 clusters
215 "hotspots": self.hotspots[:10], # Top 10 hotspots
216 }
219@dataclass
220class RegressionCluster:
221 """A cluster of bugs where fixes led to new bugs."""
223 primary_file: str # Main file in the regression chain
224 regression_count: int = 0 # Number of regression pairs
225 fix_bug_pairs: list[tuple[str, str]] = field(default_factory=list) # (fixed_id, caused_id)
226 related_files: list[str] = field(default_factory=list) # All files in chain
227 time_pattern: str = "immediate" # "immediate" (<3d), "delayed" (3-7d), "chronic" (recurring)
228 severity: str = "medium" # "critical", "high", "medium"
230 def to_dict(self) -> dict[str, Any]:
231 """Convert to dictionary for serialization."""
232 return {
233 "primary_file": self.primary_file,
234 "regression_count": self.regression_count,
235 "fix_bug_pairs": self.fix_bug_pairs[:10], # Top 10
236 "related_files": self.related_files[:10], # Top 10
237 "time_pattern": self.time_pattern,
238 "severity": self.severity,
239 }
242@dataclass
243class RegressionAnalysis:
244 """Analysis of regression patterns in bug fixes."""
246 clusters: list[RegressionCluster] = field(default_factory=list)
247 total_regression_chains: int = 0
248 most_fragile_files: list[str] = field(default_factory=list)
250 def to_dict(self) -> dict[str, Any]:
251 """Convert to dictionary for serialization."""
252 return {
253 "clusters": [c.to_dict() for c in self.clusters],
254 "total_regression_chains": self.total_regression_chains,
255 "most_fragile_files": self.most_fragile_files[:5], # Top 5
256 }
259@dataclass
260class TestGap:
261 """A source file with bugs but missing or weak test coverage."""
263 source_file: str
264 bug_count: int = 0
265 bug_ids: list[str] = field(default_factory=list)
266 has_test_file: bool = False
267 test_file_path: str | None = None
268 gap_score: float = 0.0 # bug_count * multiplier, higher = worse
269 priority: str = "low" # "critical", "high", "medium", "low"
271 def to_dict(self) -> dict[str, Any]:
272 """Convert to dictionary for serialization."""
273 return {
274 "source_file": self.source_file,
275 "bug_count": self.bug_count,
276 "bug_ids": self.bug_ids[:10], # Top 10
277 "has_test_file": self.has_test_file,
278 "test_file_path": self.test_file_path,
279 "gap_score": round(self.gap_score, 2),
280 "priority": self.priority,
281 }
284@dataclass
285class TestGapAnalysis:
286 """Analysis of test coverage gaps correlated with bug occurrences."""
288 gaps: list[TestGap] = field(default_factory=list)
289 untested_bug_magnets: list[str] = field(default_factory=list)
290 files_with_tests_avg_bugs: float = 0.0
291 files_without_tests_avg_bugs: float = 0.0
292 priority_test_targets: list[str] = field(default_factory=list)
294 def to_dict(self) -> dict[str, Any]:
295 """Convert to dictionary for serialization."""
296 return {
297 "gaps": [g.to_dict() for g in self.gaps],
298 "untested_bug_magnets": self.untested_bug_magnets[:5],
299 "files_with_tests_avg_bugs": round(self.files_with_tests_avg_bugs, 2),
300 "files_without_tests_avg_bugs": round(self.files_without_tests_avg_bugs, 2),
301 "priority_test_targets": self.priority_test_targets[:10],
302 }
305@dataclass
306class RejectionMetrics:
307 """Metrics for rejection and invalid closure tracking."""
309 total_closed: int = 0
310 rejected_count: int = 0
311 invalid_count: int = 0
312 duplicate_count: int = 0
313 deferred_count: int = 0
314 completed_count: int = 0
316 @property
317 def rejection_rate(self) -> float:
318 """Calculate rejection rate."""
319 if self.total_closed == 0:
320 return 0.0
321 return self.rejected_count / self.total_closed
323 @property
324 def invalid_rate(self) -> float:
325 """Calculate invalid rate."""
326 if self.total_closed == 0:
327 return 0.0
328 return self.invalid_count / self.total_closed
330 def to_dict(self) -> dict[str, Any]:
331 """Convert to dictionary for serialization."""
332 return {
333 "total_closed": self.total_closed,
334 "rejected_count": self.rejected_count,
335 "invalid_count": self.invalid_count,
336 "duplicate_count": self.duplicate_count,
337 "deferred_count": self.deferred_count,
338 "completed_count": self.completed_count,
339 "rejection_rate": round(self.rejection_rate, 3),
340 "invalid_rate": round(self.invalid_rate, 3),
341 }
344@dataclass
345class RejectionAnalysis:
346 """Analysis of rejection and invalid closure patterns."""
348 overall: RejectionMetrics = field(default_factory=RejectionMetrics)
349 by_type: dict[str, RejectionMetrics] = field(default_factory=dict)
350 by_month: dict[str, RejectionMetrics] = field(default_factory=dict)
351 common_reasons: list[tuple[str, int]] = field(default_factory=list)
352 trend: str = "stable" # "improving", "stable", "degrading"
354 def to_dict(self) -> dict[str, Any]:
355 """Convert to dictionary for serialization."""
356 return {
357 "overall": self.overall.to_dict(),
358 "by_type": {k: v.to_dict() for k, v in self.by_type.items()},
359 "by_month": {k: v.to_dict() for k, v in sorted(self.by_month.items())},
360 "common_reasons": self.common_reasons[:10],
361 "trend": self.trend,
362 }
365@dataclass
366class ManualPattern:
367 """A recurring manual activity detected across issues."""
369 pattern_type: str # "test", "lint", "build", "git", "verification"
370 pattern_description: str
371 occurrence_count: int = 0
372 affected_issues: list[str] = field(default_factory=list) # issue IDs
373 example_commands: list[str] = field(default_factory=list) # sample commands found
374 suggested_automation: str = "" # hook, skill, or agent suggestion
375 automation_complexity: str = "simple" # "trivial", "simple", "moderate"
377 def to_dict(self) -> dict[str, Any]:
378 """Convert to dictionary for serialization."""
379 return {
380 "pattern_type": self.pattern_type,
381 "pattern_description": self.pattern_description,
382 "occurrence_count": self.occurrence_count,
383 "affected_issues": self.affected_issues[:10],
384 "example_commands": self.example_commands[:5],
385 "suggested_automation": self.suggested_automation,
386 "automation_complexity": self.automation_complexity,
387 }
390@dataclass
391class ManualPatternAnalysis:
392 """Analysis of recurring manual activities that could be automated."""
394 patterns: list[ManualPattern] = field(default_factory=list)
395 total_manual_interventions: int = 0
396 automatable_count: int = 0
397 automation_suggestions: list[str] = field(default_factory=list)
399 @property
400 def automatable_percentage(self) -> float:
401 """Calculate percentage of patterns that are automatable."""
402 if self.total_manual_interventions == 0:
403 return 0.0
404 return self.automatable_count / self.total_manual_interventions * 100
406 def to_dict(self) -> dict[str, Any]:
407 """Convert to dictionary for serialization."""
408 return {
409 "patterns": [p.to_dict() for p in self.patterns],
410 "total_manual_interventions": self.total_manual_interventions,
411 "automatable_count": self.automatable_count,
412 "automatable_percentage": round(self.automatable_percentage, 1),
413 "automation_suggestions": self.automation_suggestions[:10],
414 }
417@dataclass
418class ConfigGap:
419 """A gap in configuration that could address recurring manual work."""
421 gap_type: str # "hook", "skill", "agent"
422 description: str
423 evidence: list[str] = field(default_factory=list) # issue IDs showing the pattern
424 suggested_config: str = "" # example configuration
425 priority: str = "medium" # "high", "medium", "low"
426 pattern_type: str = "" # links back to ManualPattern.pattern_type
428 def to_dict(self) -> dict[str, Any]:
429 """Convert to dictionary for serialization."""
430 return {
431 "gap_type": self.gap_type,
432 "description": self.description,
433 "evidence": self.evidence[:10],
434 "suggested_config": self.suggested_config,
435 "priority": self.priority,
436 "pattern_type": self.pattern_type,
437 }
440@dataclass
441class ConfigGapsAnalysis:
442 """Analysis of configuration gaps based on manual pattern detection."""
444 gaps: list[ConfigGap] = field(default_factory=list)
445 current_hooks: list[str] = field(default_factory=list)
446 current_skills: list[str] = field(default_factory=list)
447 current_agents: list[str] = field(default_factory=list)
448 coverage_score: float = 0.0 # 0-1, how well config covers common needs
450 def to_dict(self) -> dict[str, Any]:
451 """Convert to dictionary for serialization."""
452 return {
453 "gaps": [g.to_dict() for g in self.gaps],
454 "current_hooks": self.current_hooks,
455 "current_skills": self.current_skills,
456 "current_agents": self.current_agents,
457 "coverage_score": round(self.coverage_score, 2),
458 }
461@dataclass
462class AgentOutcome:
463 """Metrics for a single agent processing a specific issue type."""
465 agent_name: str
466 issue_type: str
467 success_count: int = 0
468 failure_count: int = 0
469 rejection_count: int = 0
471 @property
472 def total_count(self) -> int:
473 """Total issues handled."""
474 return self.success_count + self.failure_count + self.rejection_count
476 @property
477 def success_rate(self) -> float:
478 """Calculate success rate."""
479 if self.total_count == 0:
480 return 0.0
481 return self.success_count / self.total_count
483 def to_dict(self) -> dict[str, Any]:
484 """Convert to dictionary for serialization."""
485 return {
486 "agent_name": self.agent_name,
487 "issue_type": self.issue_type,
488 "success_count": self.success_count,
489 "failure_count": self.failure_count,
490 "rejection_count": self.rejection_count,
491 "total_count": self.total_count,
492 "success_rate": round(self.success_rate, 3),
493 }
496@dataclass
497class AgentEffectivenessAnalysis:
498 """Analysis of agent effectiveness across issue types."""
500 outcomes: list[AgentOutcome] = field(default_factory=list)
501 best_agent_by_type: dict[str, str] = field(default_factory=dict)
502 problematic_combinations: list[tuple[str, str, str]] = field(default_factory=list)
504 def to_dict(self) -> dict[str, Any]:
505 """Convert to dictionary for serialization."""
506 return {
507 "outcomes": [o.to_dict() for o in self.outcomes],
508 "best_agent_by_type": self.best_agent_by_type,
509 "problematic_combinations": self.problematic_combinations[:10],
510 }
513@dataclass
514class TechnicalDebtMetrics:
515 """Technical debt health indicators."""
517 backlog_size: int = 0 # Total open issues
518 backlog_growth_rate: float = 0.0 # Net issues/week
519 aging_30_plus: int = 0 # Issues > 30 days old
520 aging_60_plus: int = 0 # Issues > 60 days old
521 high_priority_open: int = 0 # P0-P1 open
522 debt_paydown_ratio: float = 0.0 # maintenance vs features
524 def to_dict(self) -> dict[str, Any]:
525 """Convert to dictionary for serialization."""
526 return {
527 "backlog_size": self.backlog_size,
528 "backlog_growth_rate": round(self.backlog_growth_rate, 2),
529 "aging_30_plus": self.aging_30_plus,
530 "aging_60_plus": self.aging_60_plus,
531 "high_priority_open": self.high_priority_open,
532 "debt_paydown_ratio": round(self.debt_paydown_ratio, 2),
533 }
536@dataclass
537class ComplexityProxy:
538 """Duration-based complexity proxy for a file or directory."""
540 path: str
541 avg_resolution_days: float
542 median_resolution_days: float
543 issue_count: int
544 slowest_issue: tuple[str, float] # (issue_id, days)
545 complexity_score: float # normalized 0-1
546 comparison_to_baseline: str # "2.1x baseline", etc.
548 def to_dict(self) -> dict[str, Any]:
549 """Convert to dictionary for serialization."""
550 return {
551 "path": self.path,
552 "avg_resolution_days": round(self.avg_resolution_days, 1),
553 "median_resolution_days": round(self.median_resolution_days, 1),
554 "issue_count": self.issue_count,
555 "slowest_issue": {
556 "issue_id": self.slowest_issue[0],
557 "days": round(self.slowest_issue[1], 1),
558 },
559 "complexity_score": round(self.complexity_score, 3),
560 "comparison_to_baseline": self.comparison_to_baseline,
561 }
564@dataclass
565class ComplexityProxyAnalysis:
566 """Analysis using issue duration as complexity proxy."""
568 file_complexity: list[ComplexityProxy] = field(default_factory=list)
569 directory_complexity: list[ComplexityProxy] = field(default_factory=list)
570 baseline_days: float = 0.0 # median across all issues
571 complexity_outliers: list[str] = field(default_factory=list) # files >2x baseline
573 def to_dict(self) -> dict[str, Any]:
574 """Convert to dictionary for serialization."""
575 return {
576 "file_complexity": [c.to_dict() for c in self.file_complexity[:10]],
577 "directory_complexity": [c.to_dict() for c in self.directory_complexity[:10]],
578 "baseline_days": round(self.baseline_days, 1),
579 "complexity_outliers": self.complexity_outliers[:10],
580 }
583@dataclass
584class CrossCuttingSmell:
585 """A detected cross-cutting concern scattered across the codebase."""
587 concern_type: str # "logging", "error-handling", "validation", "auth", "caching"
588 affected_directories: list[str] = field(default_factory=list)
589 issue_count: int = 0
590 issue_ids: list[str] = field(default_factory=list)
591 scatter_score: float = 0.0 # higher = more scattered (0-1)
592 suggested_pattern: str = "" # "middleware", "decorator", "aspect"
594 def to_dict(self) -> dict[str, Any]:
595 """Convert to dictionary for serialization."""
596 return {
597 "concern_type": self.concern_type,
598 "affected_directories": self.affected_directories[:10],
599 "issue_count": self.issue_count,
600 "issue_ids": self.issue_ids[:10],
601 "scatter_score": round(self.scatter_score, 2),
602 "suggested_pattern": self.suggested_pattern,
603 }
606@dataclass
607class CrossCuttingAnalysis:
608 """Analysis of cross-cutting concerns scattered across the codebase."""
610 smells: list[CrossCuttingSmell] = field(default_factory=list)
611 most_scattered_concern: str = ""
612 consolidation_opportunities: list[str] = field(default_factory=list)
614 def to_dict(self) -> dict[str, Any]:
615 """Convert to dictionary for serialization."""
616 return {
617 "smells": [s.to_dict() for s in self.smells],
618 "most_scattered_concern": self.most_scattered_concern,
619 "consolidation_opportunities": self.consolidation_opportunities[:10],
620 }
623@dataclass
624class HistoryAnalysis:
625 """Complete history analysis report."""
627 generated_date: date
628 total_completed: int
629 total_active: int
630 date_range_start: date | None
631 date_range_end: date | None
633 # Core summary (from existing HistorySummary)
634 summary: HistorySummary
636 # Trend analysis
637 period_metrics: list[PeriodMetrics] = field(default_factory=list)
638 velocity_trend: str = "stable" # "increasing", "stable", "decreasing"
639 bug_ratio_trend: str = "stable"
641 # Subsystem health
642 subsystem_health: list[SubsystemHealth] = field(default_factory=list)
644 # Hotspot analysis
645 hotspot_analysis: HotspotAnalysis | None = None
647 # Coupling analysis
648 coupling_analysis: CouplingAnalysis | None = None
650 # Regression clustering analysis
651 regression_analysis: RegressionAnalysis | None = None
653 # Test gap analysis
654 test_gap_analysis: TestGapAnalysis | None = None
656 # Rejection analysis
657 rejection_analysis: RejectionAnalysis | None = None
659 # Manual pattern analysis
660 manual_pattern_analysis: ManualPatternAnalysis | None = None
662 # Agent effectiveness analysis
663 agent_effectiveness_analysis: AgentEffectivenessAnalysis | None = None
665 # Complexity proxy analysis
666 complexity_proxy_analysis: ComplexityProxyAnalysis | None = None
668 # Configuration gaps analysis
669 config_gaps_analysis: ConfigGapsAnalysis | None = None
671 # Cross-cutting concern analysis
672 cross_cutting_analysis: CrossCuttingAnalysis | None = None
674 # Technical debt
675 debt_metrics: TechnicalDebtMetrics | None = None
677 # Comparative analysis (optional)
678 comparison_period: str | None = None # e.g., "30d"
679 previous_period: PeriodMetrics | None = None
680 current_period: PeriodMetrics | None = None
682 def to_dict(self) -> dict[str, Any]:
683 """Convert to dictionary for serialization."""
684 return {
685 "generated_date": self.generated_date.isoformat(),
686 "total_completed": self.total_completed,
687 "total_active": self.total_active,
688 "date_range_start": (
689 self.date_range_start.isoformat() if self.date_range_start else None
690 ),
691 "date_range_end": (self.date_range_end.isoformat() if self.date_range_end else None),
692 "summary": self.summary.to_dict(),
693 "period_metrics": [p.to_dict() for p in self.period_metrics],
694 "velocity_trend": self.velocity_trend,
695 "bug_ratio_trend": self.bug_ratio_trend,
696 "subsystem_health": [s.to_dict() for s in self.subsystem_health],
697 "hotspot_analysis": (
698 self.hotspot_analysis.to_dict() if self.hotspot_analysis else None
699 ),
700 "coupling_analysis": (
701 self.coupling_analysis.to_dict() if self.coupling_analysis else None
702 ),
703 "regression_analysis": (
704 self.regression_analysis.to_dict() if self.regression_analysis else None
705 ),
706 "test_gap_analysis": (
707 self.test_gap_analysis.to_dict() if self.test_gap_analysis else None
708 ),
709 "rejection_analysis": (
710 self.rejection_analysis.to_dict() if self.rejection_analysis else None
711 ),
712 "manual_pattern_analysis": (
713 self.manual_pattern_analysis.to_dict() if self.manual_pattern_analysis else None
714 ),
715 "agent_effectiveness_analysis": (
716 self.agent_effectiveness_analysis.to_dict()
717 if self.agent_effectiveness_analysis
718 else None
719 ),
720 "complexity_proxy_analysis": (
721 self.complexity_proxy_analysis.to_dict() if self.complexity_proxy_analysis else None
722 ),
723 "config_gaps_analysis": (
724 self.config_gaps_analysis.to_dict() if self.config_gaps_analysis else None
725 ),
726 "cross_cutting_analysis": (
727 self.cross_cutting_analysis.to_dict() if self.cross_cutting_analysis else None
728 ),
729 "debt_metrics": self.debt_metrics.to_dict() if self.debt_metrics else None,
730 "comparison_period": self.comparison_period,
731 "previous_period": (self.previous_period.to_dict() if self.previous_period else None),
732 "current_period": (self.current_period.to_dict() if self.current_period else None),
733 }