Coverage for little_loops / issue_history / models.py: 0%
302 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 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
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
28 def to_dict(self) -> dict[str, Any]:
29 """Convert to dictionary for JSON serialization."""
30 return {
31 "path": str(self.path),
32 "issue_type": self.issue_type,
33 "priority": self.priority,
34 "issue_id": self.issue_id,
35 "discovered_by": self.discovered_by,
36 "discovered_date": (self.discovered_date.isoformat() if self.discovered_date else None),
37 "completed_date": (self.completed_date.isoformat() if self.completed_date else None),
38 }
41@dataclass
42class HistorySummary:
43 """Summary statistics for completed issues."""
45 total_count: int
46 type_counts: dict[str, int] = field(default_factory=dict)
47 priority_counts: dict[str, int] = field(default_factory=dict)
48 discovery_counts: dict[str, int] = field(default_factory=dict)
49 earliest_date: date | None = None
50 latest_date: date | None = None
52 @property
53 def date_range_days(self) -> int | None:
54 """Calculate days between earliest and latest completion."""
55 if self.earliest_date and self.latest_date:
56 return (self.latest_date - self.earliest_date).days + 1
57 return None
59 @property
60 def velocity(self) -> float | None:
61 """Calculate issues per day."""
62 if self.date_range_days and self.date_range_days > 0:
63 return self.total_count / self.date_range_days
64 return None
66 def to_dict(self) -> dict[str, Any]:
67 """Convert to dictionary for JSON serialization."""
68 return {
69 "total_count": self.total_count,
70 "type_counts": self.type_counts,
71 "priority_counts": self.priority_counts,
72 "discovery_counts": self.discovery_counts,
73 "earliest_date": (self.earliest_date.isoformat() if self.earliest_date else None),
74 "latest_date": self.latest_date.isoformat() if self.latest_date else None,
75 "date_range_days": self.date_range_days,
76 "velocity": round(self.velocity, 2) if self.velocity else None,
77 }
80@dataclass
81class PeriodMetrics:
82 """Metrics for a specific time period."""
84 period_start: date
85 period_end: date
86 period_label: str # e.g., "Q1 2025", "Jan 2025", "Week 3"
87 total_completed: int = 0
88 type_counts: dict[str, int] = field(default_factory=dict)
89 priority_counts: dict[str, int] = field(default_factory=dict)
90 avg_completion_days: float | None = None
92 @property
93 def bug_ratio(self) -> float | None:
94 """Calculate bug percentage."""
95 if self.total_completed == 0:
96 return None
97 bug_count = self.type_counts.get("BUG", 0)
98 return bug_count / self.total_completed
100 def to_dict(self) -> dict[str, Any]:
101 """Convert to dictionary for serialization."""
102 return {
103 "period_start": self.period_start.isoformat(),
104 "period_end": self.period_end.isoformat(),
105 "period_label": self.period_label,
106 "total_completed": self.total_completed,
107 "type_counts": self.type_counts,
108 "priority_counts": self.priority_counts,
109 "bug_ratio": round(self.bug_ratio, 3) if self.bug_ratio is not None else None,
110 "avg_completion_days": (
111 round(self.avg_completion_days, 1) if self.avg_completion_days else None
112 ),
113 }
116@dataclass
117class SubsystemHealth:
118 """Health metrics for a subsystem (directory)."""
120 subsystem: str # Directory path
121 total_issues: int = 0
122 recent_issues: int = 0 # Issues in last 30 days
123 issue_ids: list[str] = field(default_factory=list)
124 trend: str = "stable" # "improving", "stable", "degrading"
126 def to_dict(self) -> dict[str, Any]:
127 """Convert to dictionary for serialization."""
128 return {
129 "subsystem": self.subsystem,
130 "total_issues": self.total_issues,
131 "recent_issues": self.recent_issues,
132 "issue_ids": self.issue_ids[:5], # Top 5
133 "trend": self.trend,
134 }
137@dataclass
138class Hotspot:
139 """A file or directory that appears in multiple issues."""
141 path: str
142 issue_count: int = 0
143 issue_ids: list[str] = field(default_factory=list)
144 issue_types: dict[str, int] = field(default_factory=dict) # {"BUG": 5, "ENH": 3}
145 bug_ratio: float = 0.0 # bugs / total issues
146 churn_indicator: str = "low" # "high", "medium", "low"
148 def to_dict(self) -> dict[str, Any]:
149 """Convert to dictionary for serialization."""
150 return {
151 "path": self.path,
152 "issue_count": self.issue_count,
153 "issue_ids": self.issue_ids[:10], # Top 10
154 "issue_types": self.issue_types,
155 "bug_ratio": round(self.bug_ratio, 3),
156 "churn_indicator": self.churn_indicator,
157 }
160@dataclass
161class HotspotAnalysis:
162 """Analysis of files and directories appearing repeatedly in issues."""
164 file_hotspots: list[Hotspot] = field(default_factory=list)
165 directory_hotspots: list[Hotspot] = field(default_factory=list)
166 bug_magnets: list[Hotspot] = field(default_factory=list) # >60% bug ratio
168 def to_dict(self) -> dict[str, Any]:
169 """Convert to dictionary for serialization."""
170 return {
171 "file_hotspots": [h.to_dict() for h in self.file_hotspots],
172 "directory_hotspots": [h.to_dict() for h in self.directory_hotspots],
173 "bug_magnets": [h.to_dict() for h in self.bug_magnets],
174 }
177@dataclass
178class CouplingPair:
179 """A pair of files that frequently appear together in issues."""
181 file_a: str
182 file_b: str
183 co_occurrence_count: int = 0
184 coupling_strength: float = 0.0 # 0-1, Jaccard similarity
185 issue_ids: list[str] = field(default_factory=list)
187 def to_dict(self) -> dict[str, Any]:
188 """Convert to dictionary for serialization."""
189 return {
190 "file_a": self.file_a,
191 "file_b": self.file_b,
192 "co_occurrence_count": self.co_occurrence_count,
193 "coupling_strength": round(self.coupling_strength, 3),
194 "issue_ids": self.issue_ids[:10], # Top 10
195 }
198@dataclass
199class CouplingAnalysis:
200 """Analysis of files that frequently change together."""
202 pairs: list[CouplingPair] = field(default_factory=list)
203 clusters: list[list[str]] = field(default_factory=list) # Groups of coupled files
204 hotspots: list[str] = field(default_factory=list) # Files coupled with 3+ others
206 def to_dict(self) -> dict[str, Any]:
207 """Convert to dictionary for serialization."""
208 return {
209 "pairs": [p.to_dict() for p in self.pairs],
210 "clusters": self.clusters[:10], # Top 10 clusters
211 "hotspots": self.hotspots[:10], # Top 10 hotspots
212 }
215@dataclass
216class RegressionCluster:
217 """A cluster of bugs where fixes led to new bugs."""
219 primary_file: str # Main file in the regression chain
220 regression_count: int = 0 # Number of regression pairs
221 fix_bug_pairs: list[tuple[str, str]] = field(default_factory=list) # (fixed_id, caused_id)
222 related_files: list[str] = field(default_factory=list) # All files in chain
223 time_pattern: str = "immediate" # "immediate" (<3d), "delayed" (3-7d), "chronic" (recurring)
224 severity: str = "medium" # "critical", "high", "medium"
226 def to_dict(self) -> dict[str, Any]:
227 """Convert to dictionary for serialization."""
228 return {
229 "primary_file": self.primary_file,
230 "regression_count": self.regression_count,
231 "fix_bug_pairs": self.fix_bug_pairs[:10], # Top 10
232 "related_files": self.related_files[:10], # Top 10
233 "time_pattern": self.time_pattern,
234 "severity": self.severity,
235 }
238@dataclass
239class RegressionAnalysis:
240 """Analysis of regression patterns in bug fixes."""
242 clusters: list[RegressionCluster] = field(default_factory=list)
243 total_regression_chains: int = 0
244 most_fragile_files: list[str] = field(default_factory=list)
246 def to_dict(self) -> dict[str, Any]:
247 """Convert to dictionary for serialization."""
248 return {
249 "clusters": [c.to_dict() for c in self.clusters],
250 "total_regression_chains": self.total_regression_chains,
251 "most_fragile_files": self.most_fragile_files[:5], # Top 5
252 }
255@dataclass
256class TestGap:
257 """A source file with bugs but missing or weak test coverage."""
259 source_file: str
260 bug_count: int = 0
261 bug_ids: list[str] = field(default_factory=list)
262 has_test_file: bool = False
263 test_file_path: str | None = None
264 gap_score: float = 0.0 # bug_count * multiplier, higher = worse
265 priority: str = "low" # "critical", "high", "medium", "low"
267 def to_dict(self) -> dict[str, Any]:
268 """Convert to dictionary for serialization."""
269 return {
270 "source_file": self.source_file,
271 "bug_count": self.bug_count,
272 "bug_ids": self.bug_ids[:10], # Top 10
273 "has_test_file": self.has_test_file,
274 "test_file_path": self.test_file_path,
275 "gap_score": round(self.gap_score, 2),
276 "priority": self.priority,
277 }
280@dataclass
281class TestGapAnalysis:
282 """Analysis of test coverage gaps correlated with bug occurrences."""
284 gaps: list[TestGap] = field(default_factory=list)
285 untested_bug_magnets: list[str] = field(default_factory=list)
286 files_with_tests_avg_bugs: float = 0.0
287 files_without_tests_avg_bugs: float = 0.0
288 priority_test_targets: list[str] = field(default_factory=list)
290 def to_dict(self) -> dict[str, Any]:
291 """Convert to dictionary for serialization."""
292 return {
293 "gaps": [g.to_dict() for g in self.gaps],
294 "untested_bug_magnets": self.untested_bug_magnets[:5],
295 "files_with_tests_avg_bugs": round(self.files_with_tests_avg_bugs, 2),
296 "files_without_tests_avg_bugs": round(self.files_without_tests_avg_bugs, 2),
297 "priority_test_targets": self.priority_test_targets[:10],
298 }
301@dataclass
302class RejectionMetrics:
303 """Metrics for rejection and invalid closure tracking."""
305 total_closed: int = 0
306 rejected_count: int = 0
307 invalid_count: int = 0
308 duplicate_count: int = 0
309 deferred_count: int = 0
310 completed_count: int = 0
312 @property
313 def rejection_rate(self) -> float:
314 """Calculate rejection rate."""
315 if self.total_closed == 0:
316 return 0.0
317 return self.rejected_count / self.total_closed
319 @property
320 def invalid_rate(self) -> float:
321 """Calculate invalid rate."""
322 if self.total_closed == 0:
323 return 0.0
324 return self.invalid_count / self.total_closed
326 def to_dict(self) -> dict[str, Any]:
327 """Convert to dictionary for serialization."""
328 return {
329 "total_closed": self.total_closed,
330 "rejected_count": self.rejected_count,
331 "invalid_count": self.invalid_count,
332 "duplicate_count": self.duplicate_count,
333 "deferred_count": self.deferred_count,
334 "completed_count": self.completed_count,
335 "rejection_rate": round(self.rejection_rate, 3),
336 "invalid_rate": round(self.invalid_rate, 3),
337 }
340@dataclass
341class RejectionAnalysis:
342 """Analysis of rejection and invalid closure patterns."""
344 overall: RejectionMetrics = field(default_factory=RejectionMetrics)
345 by_type: dict[str, RejectionMetrics] = field(default_factory=dict)
346 by_month: dict[str, RejectionMetrics] = field(default_factory=dict)
347 common_reasons: list[tuple[str, int]] = field(default_factory=list)
348 trend: str = "stable" # "improving", "stable", "degrading"
350 def to_dict(self) -> dict[str, Any]:
351 """Convert to dictionary for serialization."""
352 return {
353 "overall": self.overall.to_dict(),
354 "by_type": {k: v.to_dict() for k, v in self.by_type.items()},
355 "by_month": {k: v.to_dict() for k, v in sorted(self.by_month.items())},
356 "common_reasons": self.common_reasons[:10],
357 "trend": self.trend,
358 }
361@dataclass
362class ManualPattern:
363 """A recurring manual activity detected across issues."""
365 pattern_type: str # "test", "lint", "build", "git", "verification"
366 pattern_description: str
367 occurrence_count: int = 0
368 affected_issues: list[str] = field(default_factory=list) # issue IDs
369 example_commands: list[str] = field(default_factory=list) # sample commands found
370 suggested_automation: str = "" # hook, skill, or agent suggestion
371 automation_complexity: str = "simple" # "trivial", "simple", "moderate"
373 def to_dict(self) -> dict[str, Any]:
374 """Convert to dictionary for serialization."""
375 return {
376 "pattern_type": self.pattern_type,
377 "pattern_description": self.pattern_description,
378 "occurrence_count": self.occurrence_count,
379 "affected_issues": self.affected_issues[:10],
380 "example_commands": self.example_commands[:5],
381 "suggested_automation": self.suggested_automation,
382 "automation_complexity": self.automation_complexity,
383 }
386@dataclass
387class ManualPatternAnalysis:
388 """Analysis of recurring manual activities that could be automated."""
390 patterns: list[ManualPattern] = field(default_factory=list)
391 total_manual_interventions: int = 0
392 automatable_count: int = 0
393 automation_suggestions: list[str] = field(default_factory=list)
395 @property
396 def automatable_percentage(self) -> float:
397 """Calculate percentage of patterns that are automatable."""
398 if self.total_manual_interventions == 0:
399 return 0.0
400 return self.automatable_count / self.total_manual_interventions * 100
402 def to_dict(self) -> dict[str, Any]:
403 """Convert to dictionary for serialization."""
404 return {
405 "patterns": [p.to_dict() for p in self.patterns],
406 "total_manual_interventions": self.total_manual_interventions,
407 "automatable_count": self.automatable_count,
408 "automatable_percentage": round(self.automatable_percentage, 1),
409 "automation_suggestions": self.automation_suggestions[:10],
410 }
413@dataclass
414class ConfigGap:
415 """A gap in configuration that could address recurring manual work."""
417 gap_type: str # "hook", "skill", "agent"
418 description: str
419 evidence: list[str] = field(default_factory=list) # issue IDs showing the pattern
420 suggested_config: str = "" # example configuration
421 priority: str = "medium" # "high", "medium", "low"
422 pattern_type: str = "" # links back to ManualPattern.pattern_type
424 def to_dict(self) -> dict[str, Any]:
425 """Convert to dictionary for serialization."""
426 return {
427 "gap_type": self.gap_type,
428 "description": self.description,
429 "evidence": self.evidence[:10],
430 "suggested_config": self.suggested_config,
431 "priority": self.priority,
432 "pattern_type": self.pattern_type,
433 }
436@dataclass
437class ConfigGapsAnalysis:
438 """Analysis of configuration gaps based on manual pattern detection."""
440 gaps: list[ConfigGap] = field(default_factory=list)
441 current_hooks: list[str] = field(default_factory=list)
442 current_skills: list[str] = field(default_factory=list)
443 current_agents: list[str] = field(default_factory=list)
444 coverage_score: float = 0.0 # 0-1, how well config covers common needs
446 def to_dict(self) -> dict[str, Any]:
447 """Convert to dictionary for serialization."""
448 return {
449 "gaps": [g.to_dict() for g in self.gaps],
450 "current_hooks": self.current_hooks,
451 "current_skills": self.current_skills,
452 "current_agents": self.current_agents,
453 "coverage_score": round(self.coverage_score, 2),
454 }
457@dataclass
458class AgentOutcome:
459 """Metrics for a single agent processing a specific issue type."""
461 agent_name: str
462 issue_type: str
463 success_count: int = 0
464 failure_count: int = 0
465 rejection_count: int = 0
467 @property
468 def total_count(self) -> int:
469 """Total issues handled."""
470 return self.success_count + self.failure_count + self.rejection_count
472 @property
473 def success_rate(self) -> float:
474 """Calculate success rate."""
475 if self.total_count == 0:
476 return 0.0
477 return self.success_count / self.total_count
479 def to_dict(self) -> dict[str, Any]:
480 """Convert to dictionary for serialization."""
481 return {
482 "agent_name": self.agent_name,
483 "issue_type": self.issue_type,
484 "success_count": self.success_count,
485 "failure_count": self.failure_count,
486 "rejection_count": self.rejection_count,
487 "total_count": self.total_count,
488 "success_rate": round(self.success_rate, 3),
489 }
492@dataclass
493class AgentEffectivenessAnalysis:
494 """Analysis of agent effectiveness across issue types."""
496 outcomes: list[AgentOutcome] = field(default_factory=list)
497 best_agent_by_type: dict[str, str] = field(default_factory=dict)
498 problematic_combinations: list[tuple[str, str, str]] = field(default_factory=list)
500 def to_dict(self) -> dict[str, Any]:
501 """Convert to dictionary for serialization."""
502 return {
503 "outcomes": [o.to_dict() for o in self.outcomes],
504 "best_agent_by_type": self.best_agent_by_type,
505 "problematic_combinations": self.problematic_combinations[:10],
506 }
509@dataclass
510class TechnicalDebtMetrics:
511 """Technical debt health indicators."""
513 backlog_size: int = 0 # Total open issues
514 backlog_growth_rate: float = 0.0 # Net issues/week
515 aging_30_plus: int = 0 # Issues > 30 days old
516 aging_60_plus: int = 0 # Issues > 60 days old
517 high_priority_open: int = 0 # P0-P1 open
518 debt_paydown_ratio: float = 0.0 # maintenance vs features
520 def to_dict(self) -> dict[str, Any]:
521 """Convert to dictionary for serialization."""
522 return {
523 "backlog_size": self.backlog_size,
524 "backlog_growth_rate": round(self.backlog_growth_rate, 2),
525 "aging_30_plus": self.aging_30_plus,
526 "aging_60_plus": self.aging_60_plus,
527 "high_priority_open": self.high_priority_open,
528 "debt_paydown_ratio": round(self.debt_paydown_ratio, 2),
529 }
532@dataclass
533class ComplexityProxy:
534 """Duration-based complexity proxy for a file or directory."""
536 path: str
537 avg_resolution_days: float
538 median_resolution_days: float
539 issue_count: int
540 slowest_issue: tuple[str, float] # (issue_id, days)
541 complexity_score: float # normalized 0-1
542 comparison_to_baseline: str # "2.1x baseline", etc.
544 def to_dict(self) -> dict[str, Any]:
545 """Convert to dictionary for serialization."""
546 return {
547 "path": self.path,
548 "avg_resolution_days": round(self.avg_resolution_days, 1),
549 "median_resolution_days": round(self.median_resolution_days, 1),
550 "issue_count": self.issue_count,
551 "slowest_issue": {
552 "issue_id": self.slowest_issue[0],
553 "days": round(self.slowest_issue[1], 1),
554 },
555 "complexity_score": round(self.complexity_score, 3),
556 "comparison_to_baseline": self.comparison_to_baseline,
557 }
560@dataclass
561class ComplexityProxyAnalysis:
562 """Analysis using issue duration as complexity proxy."""
564 file_complexity: list[ComplexityProxy] = field(default_factory=list)
565 directory_complexity: list[ComplexityProxy] = field(default_factory=list)
566 baseline_days: float = 0.0 # median across all issues
567 complexity_outliers: list[str] = field(default_factory=list) # files >2x baseline
569 def to_dict(self) -> dict[str, Any]:
570 """Convert to dictionary for serialization."""
571 return {
572 "file_complexity": [c.to_dict() for c in self.file_complexity[:10]],
573 "directory_complexity": [c.to_dict() for c in self.directory_complexity[:10]],
574 "baseline_days": round(self.baseline_days, 1),
575 "complexity_outliers": self.complexity_outliers[:10],
576 }
579@dataclass
580class CrossCuttingSmell:
581 """A detected cross-cutting concern scattered across the codebase."""
583 concern_type: str # "logging", "error-handling", "validation", "auth", "caching"
584 affected_directories: list[str] = field(default_factory=list)
585 issue_count: int = 0
586 issue_ids: list[str] = field(default_factory=list)
587 scatter_score: float = 0.0 # higher = more scattered (0-1)
588 suggested_pattern: str = "" # "middleware", "decorator", "aspect"
590 def to_dict(self) -> dict[str, Any]:
591 """Convert to dictionary for serialization."""
592 return {
593 "concern_type": self.concern_type,
594 "affected_directories": self.affected_directories[:10],
595 "issue_count": self.issue_count,
596 "issue_ids": self.issue_ids[:10],
597 "scatter_score": round(self.scatter_score, 2),
598 "suggested_pattern": self.suggested_pattern,
599 }
602@dataclass
603class CrossCuttingAnalysis:
604 """Analysis of cross-cutting concerns scattered across the codebase."""
606 smells: list[CrossCuttingSmell] = field(default_factory=list)
607 most_scattered_concern: str = ""
608 consolidation_opportunities: list[str] = field(default_factory=list)
610 def to_dict(self) -> dict[str, Any]:
611 """Convert to dictionary for serialization."""
612 return {
613 "smells": [s.to_dict() for s in self.smells],
614 "most_scattered_concern": self.most_scattered_concern,
615 "consolidation_opportunities": self.consolidation_opportunities[:10],
616 }
619@dataclass
620class HistoryAnalysis:
621 """Complete history analysis report."""
623 generated_date: date
624 total_completed: int
625 total_active: int
626 date_range_start: date | None
627 date_range_end: date | None
629 # Core summary (from existing HistorySummary)
630 summary: HistorySummary
632 # Trend analysis
633 period_metrics: list[PeriodMetrics] = field(default_factory=list)
634 velocity_trend: str = "stable" # "increasing", "stable", "decreasing"
635 bug_ratio_trend: str = "stable"
637 # Subsystem health
638 subsystem_health: list[SubsystemHealth] = field(default_factory=list)
640 # Hotspot analysis
641 hotspot_analysis: HotspotAnalysis | None = None
643 # Coupling analysis
644 coupling_analysis: CouplingAnalysis | None = None
646 # Regression clustering analysis
647 regression_analysis: RegressionAnalysis | None = None
649 # Test gap analysis
650 test_gap_analysis: TestGapAnalysis | None = None
652 # Rejection analysis
653 rejection_analysis: RejectionAnalysis | None = None
655 # Manual pattern analysis
656 manual_pattern_analysis: ManualPatternAnalysis | None = None
658 # Agent effectiveness analysis
659 agent_effectiveness_analysis: AgentEffectivenessAnalysis | None = None
661 # Complexity proxy analysis
662 complexity_proxy_analysis: ComplexityProxyAnalysis | None = None
664 # Configuration gaps analysis
665 config_gaps_analysis: ConfigGapsAnalysis | None = None
667 # Cross-cutting concern analysis
668 cross_cutting_analysis: CrossCuttingAnalysis | None = None
670 # Technical debt
671 debt_metrics: TechnicalDebtMetrics | None = None
673 # Comparative analysis (optional)
674 comparison_period: str | None = None # e.g., "30d"
675 previous_period: PeriodMetrics | None = None
676 current_period: PeriodMetrics | None = None
678 def to_dict(self) -> dict[str, Any]:
679 """Convert to dictionary for serialization."""
680 return {
681 "generated_date": self.generated_date.isoformat(),
682 "total_completed": self.total_completed,
683 "total_active": self.total_active,
684 "date_range_start": (
685 self.date_range_start.isoformat() if self.date_range_start else None
686 ),
687 "date_range_end": (self.date_range_end.isoformat() if self.date_range_end else None),
688 "summary": self.summary.to_dict(),
689 "period_metrics": [p.to_dict() for p in self.period_metrics],
690 "velocity_trend": self.velocity_trend,
691 "bug_ratio_trend": self.bug_ratio_trend,
692 "subsystem_health": [s.to_dict() for s in self.subsystem_health],
693 "hotspot_analysis": (
694 self.hotspot_analysis.to_dict() if self.hotspot_analysis else None
695 ),
696 "coupling_analysis": (
697 self.coupling_analysis.to_dict() if self.coupling_analysis else None
698 ),
699 "regression_analysis": (
700 self.regression_analysis.to_dict() if self.regression_analysis else None
701 ),
702 "test_gap_analysis": (
703 self.test_gap_analysis.to_dict() if self.test_gap_analysis else None
704 ),
705 "rejection_analysis": (
706 self.rejection_analysis.to_dict() if self.rejection_analysis else None
707 ),
708 "manual_pattern_analysis": (
709 self.manual_pattern_analysis.to_dict() if self.manual_pattern_analysis else None
710 ),
711 "agent_effectiveness_analysis": (
712 self.agent_effectiveness_analysis.to_dict()
713 if self.agent_effectiveness_analysis
714 else None
715 ),
716 "complexity_proxy_analysis": (
717 self.complexity_proxy_analysis.to_dict() if self.complexity_proxy_analysis else None
718 ),
719 "config_gaps_analysis": (
720 self.config_gaps_analysis.to_dict() if self.config_gaps_analysis else None
721 ),
722 "cross_cutting_analysis": (
723 self.cross_cutting_analysis.to_dict() if self.cross_cutting_analysis else None
724 ),
725 "debt_metrics": self.debt_metrics.to_dict() if self.debt_metrics else None,
726 "comparison_period": self.comparison_period,
727 "previous_period": (self.previous_period.to_dict() if self.previous_period else None),
728 "current_period": (self.current_period.to_dict() if self.current_period else None),
729 }