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

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 

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 

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 

42class 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 

81class 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 

117class 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 

138class 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 

161class 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 

178class 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 

199class 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 

216class 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 

239class 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 

256class 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 

281class 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 

302class 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 

341class 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 

362class 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 

387class 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 

414class 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 

437class 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 

458class 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 

493class 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 

510class 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 

533class 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 

561class 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 

580class 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 

603class 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 

620class 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 }