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

1"""Issue history data models. 

2 

3Dataclasses for issue history analysis including completed issues, 

4summary statistics, hotspot detection, coupling analysis, regression 

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

6""" 

7 

8from __future__ import annotations 

9 

10from dataclasses import dataclass, field 

11from datetime import date, datetime 

12from pathlib import Path 

13from typing import Any 

14 

15 

16@dataclass 

17class 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 captured_at: datetime | None = None 

28 completed_at: datetime | None = None 

29 

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 } 

43 

44 

45@dataclass 

46class HistorySummary: 

47 """Summary statistics for completed issues.""" 

48 

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 

55 

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 

62 

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 

69 

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 } 

82 

83 

84@dataclass 

85class PeriodMetrics: 

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

87 

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 

95 

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 

103 

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 } 

118 

119 

120@dataclass 

121class SubsystemHealth: 

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

123 

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" 

129 

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 } 

139 

140 

141@dataclass 

142class Hotspot: 

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

144 

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" 

151 

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 } 

162 

163 

164@dataclass 

165class HotspotAnalysis: 

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

167 

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 

171 

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 } 

179 

180 

181@dataclass 

182class CouplingPair: 

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

184 

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) 

190 

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 } 

200 

201 

202@dataclass 

203class CouplingAnalysis: 

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

205 

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 

209 

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 } 

217 

218 

219@dataclass 

220class RegressionCluster: 

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

222 

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" 

229 

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 } 

240 

241 

242@dataclass 

243class RegressionAnalysis: 

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

245 

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

247 total_regression_chains: int = 0 

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

249 

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 } 

257 

258 

259@dataclass 

260class TestGap: 

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

262 

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" 

270 

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 } 

282 

283 

284@dataclass 

285class TestGapAnalysis: 

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

287 

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) 

293 

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 } 

303 

304 

305@dataclass 

306class RejectionMetrics: 

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

308 

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 

315 

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 

322 

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 

329 

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 } 

342 

343 

344@dataclass 

345class RejectionAnalysis: 

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

347 

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" 

353 

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 } 

363 

364 

365@dataclass 

366class ManualPattern: 

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

368 

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" 

376 

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 } 

388 

389 

390@dataclass 

391class ManualPatternAnalysis: 

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

393 

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) 

398 

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 

405 

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 } 

415 

416 

417@dataclass 

418class ConfigGap: 

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

420 

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 

427 

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 } 

438 

439 

440@dataclass 

441class ConfigGapsAnalysis: 

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

443 

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 

449 

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 } 

459 

460 

461@dataclass 

462class AgentOutcome: 

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

464 

465 agent_name: str 

466 issue_type: str 

467 success_count: int = 0 

468 failure_count: int = 0 

469 rejection_count: int = 0 

470 

471 @property 

472 def total_count(self) -> int: 

473 """Total issues handled.""" 

474 return self.success_count + self.failure_count + self.rejection_count 

475 

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 

482 

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 } 

494 

495 

496@dataclass 

497class AgentEffectivenessAnalysis: 

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

499 

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) 

503 

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 } 

511 

512 

513@dataclass 

514class TechnicalDebtMetrics: 

515 """Technical debt health indicators.""" 

516 

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 

523 

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 } 

534 

535 

536@dataclass 

537class ComplexityProxy: 

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

539 

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. 

547 

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 } 

562 

563 

564@dataclass 

565class ComplexityProxyAnalysis: 

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

567 

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 

572 

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 } 

581 

582 

583@dataclass 

584class CrossCuttingSmell: 

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

586 

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" 

593 

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 } 

604 

605 

606@dataclass 

607class CrossCuttingAnalysis: 

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

609 

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

611 most_scattered_concern: str = "" 

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

613 

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 } 

621 

622 

623@dataclass 

624class HistoryAnalysis: 

625 """Complete history analysis report.""" 

626 

627 generated_date: date 

628 total_completed: int 

629 total_active: int 

630 date_range_start: date | None 

631 date_range_end: date | None 

632 

633 # Core summary (from existing HistorySummary) 

634 summary: HistorySummary 

635 

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" 

640 

641 # Subsystem health 

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

643 

644 # Hotspot analysis 

645 hotspot_analysis: HotspotAnalysis | None = None 

646 

647 # Coupling analysis 

648 coupling_analysis: CouplingAnalysis | None = None 

649 

650 # Regression clustering analysis 

651 regression_analysis: RegressionAnalysis | None = None 

652 

653 # Test gap analysis 

654 test_gap_analysis: TestGapAnalysis | None = None 

655 

656 # Rejection analysis 

657 rejection_analysis: RejectionAnalysis | None = None 

658 

659 # Manual pattern analysis 

660 manual_pattern_analysis: ManualPatternAnalysis | None = None 

661 

662 # Agent effectiveness analysis 

663 agent_effectiveness_analysis: AgentEffectivenessAnalysis | None = None 

664 

665 # Complexity proxy analysis 

666 complexity_proxy_analysis: ComplexityProxyAnalysis | None = None 

667 

668 # Configuration gaps analysis 

669 config_gaps_analysis: ConfigGapsAnalysis | None = None 

670 

671 # Cross-cutting concern analysis 

672 cross_cutting_analysis: CrossCuttingAnalysis | None = None 

673 

674 # Technical debt 

675 debt_metrics: TechnicalDebtMetrics | None = None 

676 

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 

681 

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 }