Coverage for little_loops/issue_history/models.py: 0%

1"""Issue history data models.

2

3 Dataclasses for issue history analysis including completed issues,

4 summary statistics, hotspot detection, coupling analysis, regression

5 clustering, test gap analysis, and technical debt metrics.

6"""

7

8 from __future__ import annotations

9

10 from dataclasses import dataclass, field

11 from datetime import date

12 from pathlib import Path

13 from typing import Any

14

15

16@dataclass

17 class CompletedIssue:

18 """Parsed information from a completed issue file."""

19

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

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 }

39

40

41@dataclass

42 class HistorySummary:

43 """Summary statistics for completed issues."""

44

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

51

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

58

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

65

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 }

78

79

80@dataclass

81 class PeriodMetrics:

82 """Metrics for a specific time period."""

83

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

91

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

99

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 }

114

115

116@dataclass

117 class SubsystemHealth:

118 """Health metrics for a subsystem (directory)."""

119

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"

125

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 }

135

136

137@dataclass

138 class Hotspot:

139 """A file or directory that appears in multiple issues."""

140

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"

147

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 }

158

159

160@dataclass

161 class HotspotAnalysis:

162 """Analysis of files and directories appearing repeatedly in issues."""

163

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

167

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 }

175

176

177@dataclass

178 class CouplingPair:

179 """A pair of files that frequently appear together in issues."""

180

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)

186

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 }

196

197

198@dataclass

199 class CouplingAnalysis:

200 """Analysis of files that frequently change together."""

201

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

205

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 }

213

214

215@dataclass

216 class RegressionCluster:

217 """A cluster of bugs where fixes led to new bugs."""

218

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"

225

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 }

236

237

238@dataclass

239 class RegressionAnalysis:

240 """Analysis of regression patterns in bug fixes."""

241

242 clusters: list[RegressionCluster] = field(default_factory=list)

243 total_regression_chains: int = 0

244 most_fragile_files: list[str] = field(default_factory=list)

245

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 }

253

254

255@dataclass

256 class TestGap:

257 """A source file with bugs but missing or weak test coverage."""

258

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"

266

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 }

278

279

280@dataclass

281 class TestGapAnalysis:

282 """Analysis of test coverage gaps correlated with bug occurrences."""

283

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)

289

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 }

299

300

301@dataclass

302 class RejectionMetrics:

303 """Metrics for rejection and invalid closure tracking."""

304

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

311

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

318

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

325

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 }

338

339

340@dataclass

341 class RejectionAnalysis:

342 """Analysis of rejection and invalid closure patterns."""

343

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"

349

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 }

359

360

361@dataclass

362 class ManualPattern:

363 """A recurring manual activity detected across issues."""

364

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"

372

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 }

384

385

386@dataclass

387 class ManualPatternAnalysis:

388 """Analysis of recurring manual activities that could be automated."""

389

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)

394

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

401

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 }

411

412

413@dataclass

414 class ConfigGap:

415 """A gap in configuration that could address recurring manual work."""

416

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

423

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 }

434

435

436@dataclass

437 class ConfigGapsAnalysis:

438 """Analysis of configuration gaps based on manual pattern detection."""

439

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

445

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 }

455

456

457@dataclass

458 class AgentOutcome:

459 """Metrics for a single agent processing a specific issue type."""

460

461 agent_name: str

462 issue_type: str

463 success_count: int = 0

464 failure_count: int = 0

465 rejection_count: int = 0

466

467 @property

468 def total_count(self) -> int:

469 """Total issues handled."""

470 return self.success_count + self.failure_count + self.rejection_count

471

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

478

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 }

490

491

492@dataclass

493 class AgentEffectivenessAnalysis:

494 """Analysis of agent effectiveness across issue types."""

495

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)

499

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 }

507

508

509@dataclass

510 class TechnicalDebtMetrics:

511 """Technical debt health indicators."""

512

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

519

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 }

530

531

532@dataclass

533 class ComplexityProxy:

534 """Duration-based complexity proxy for a file or directory."""

535

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.

543

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 }

558

559

560@dataclass

561 class ComplexityProxyAnalysis:

562 """Analysis using issue duration as complexity proxy."""

563

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

568

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 }

577

578

579@dataclass

580 class CrossCuttingSmell:

581 """A detected cross-cutting concern scattered across the codebase."""

582

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"

589

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 }

600

601

602@dataclass

603 class CrossCuttingAnalysis:

604 """Analysis of cross-cutting concerns scattered across the codebase."""

605

606 smells: list[CrossCuttingSmell] = field(default_factory=list)

607 most_scattered_concern: str = ""

608 consolidation_opportunities: list[str] = field(default_factory=list)

609

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 }

617

618

619@dataclass

620 class HistoryAnalysis:

621 """Complete history analysis report."""

622

623 generated_date: date

624 total_completed: int

625 total_active: int

626 date_range_start: date | None

627 date_range_end: date | None

628

629 # Core summary (from existing HistorySummary)

630 summary: HistorySummary

631

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"

636

637 # Subsystem health

638 subsystem_health: list[SubsystemHealth] = field(default_factory=list)

639

640 # Hotspot analysis

641 hotspot_analysis: HotspotAnalysis | None = None

642

643 # Coupling analysis

644 coupling_analysis: CouplingAnalysis | None = None

645

646 # Regression clustering analysis

647 regression_analysis: RegressionAnalysis | None = None

648

649 # Test gap analysis

650 test_gap_analysis: TestGapAnalysis | None = None

651

652 # Rejection analysis

653 rejection_analysis: RejectionAnalysis | None = None

654

655 # Manual pattern analysis

656 manual_pattern_analysis: ManualPatternAnalysis | None = None

657

658 # Agent effectiveness analysis

659 agent_effectiveness_analysis: AgentEffectivenessAnalysis | None = None

660

661 # Complexity proxy analysis

662 complexity_proxy_analysis: ComplexityProxyAnalysis | None = None

663

664 # Configuration gaps analysis

665 config_gaps_analysis: ConfigGapsAnalysis | None = None

666

667 # Cross-cutting concern analysis

668 cross_cutting_analysis: CrossCuttingAnalysis | None = None

669

670 # Technical debt

671 debt_metrics: TechnicalDebtMetrics | None = None

672

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

677

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 }