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

342 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-16 13:08 -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 RecurringFeedback: 

419 """A user correction that has recurred across multiple sessions.""" 

420 

421 topic: str # content excerpt or cluster key 

422 occurrence_count: int = 0 

423 example_sessions: list[str] = field(default_factory=list) # capped at 5 

424 example_content: list[str] = field(default_factory=list) # capped at 3 

425 candidate_rule: str = "" # proposed CLAUDE.md rule text 

426 topic_fingerprint: str = "" # sha256[:16] of raw correction content (ENH-2046) 

427 

428 def to_dict(self) -> dict[str, Any]: 

429 """Convert to dictionary for serialization.""" 

430 return { 

431 "topic": self.topic, 

432 "occurrence_count": self.occurrence_count, 

433 "example_sessions": self.example_sessions[:5], 

434 "example_content": self.example_content[:3], 

435 "candidate_rule": self.candidate_rule, 

436 "topic_fingerprint": self.topic_fingerprint, 

437 } 

438 

439 

440@dataclass 

441class RecurringFeedbackAnalysis: 

442 """Analysis of recurring user corrections that could become permanent rules.""" 

443 

444 feedbacks: list[RecurringFeedback] = field(default_factory=list) 

445 total_recurring_corrections: int = 0 

446 threshold_used: int = 2 

447 rule_candidates: list[str] = field(default_factory=list) # capped at 10 

448 retired_count: int = 0 # clusters excluded because they have retirement records (ENH-2046) 

449 

450 def to_dict(self) -> dict[str, Any]: 

451 """Convert to dictionary for serialization.""" 

452 return { 

453 "feedbacks": [f.to_dict() for f in self.feedbacks], 

454 "total_recurring_corrections": self.total_recurring_corrections, 

455 "threshold_used": self.threshold_used, 

456 "rule_candidates": self.rule_candidates[:10], 

457 "retired_count": self.retired_count, 

458 } 

459 

460 

461@dataclass 

462class SkillBypass: 

463 """A skill that the user repeatedly performed manually instead of invoking.""" 

464 

465 skill_name: str # skill that was bypassed 

466 bypass_count: int = 0 

467 example_sessions: list[str] = field(default_factory=list) # capped at 5 

468 evidence: list[str] = field(default_factory=list) # user message snippets, capped at 3 

469 suggested_improvement: str = "" # sharper trigger or lighter skill suggestion 

470 

471 def to_dict(self) -> dict[str, Any]: 

472 """Convert to dictionary for serialization.""" 

473 return { 

474 "skill_name": self.skill_name, 

475 "bypass_count": self.bypass_count, 

476 "example_sessions": self.example_sessions[:5], 

477 "evidence": self.evidence[:3], 

478 "suggested_improvement": self.suggested_improvement, 

479 } 

480 

481 

482@dataclass 

483class SkillBypassAnalysis: 

484 """Analysis of skills users bypassed by doing the work manually.""" 

485 

486 bypasses: list[SkillBypass] = field(default_factory=list) 

487 total_bypassed_invocations: int = 0 

488 threshold_used: int = 2 

489 improvement_suggestions: list[str] = field(default_factory=list) # capped at 10 

490 

491 def to_dict(self) -> dict[str, Any]: 

492 """Convert to dictionary for serialization.""" 

493 return { 

494 "bypasses": [b.to_dict() for b in self.bypasses], 

495 "total_bypassed_invocations": self.total_bypassed_invocations, 

496 "threshold_used": self.threshold_used, 

497 "improvement_suggestions": self.improvement_suggestions[:10], 

498 } 

499 

500 

501@dataclass 

502class ConfigGap: 

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

504 

505 gap_type: str # "hook", "skill", "agent" 

506 description: str 

507 evidence: list[str] = field(default_factory=list) # issue IDs showing the pattern 

508 suggested_config: str = "" # example configuration 

509 priority: str = "medium" # "high", "medium", "low" 

510 pattern_type: str = "" # links back to ManualPattern.pattern_type 

511 

512 def to_dict(self) -> dict[str, Any]: 

513 """Convert to dictionary for serialization.""" 

514 return { 

515 "gap_type": self.gap_type, 

516 "description": self.description, 

517 "evidence": self.evidence[:10], 

518 "suggested_config": self.suggested_config, 

519 "priority": self.priority, 

520 "pattern_type": self.pattern_type, 

521 } 

522 

523 

524@dataclass 

525class ConfigGapsAnalysis: 

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

527 

528 gaps: list[ConfigGap] = field(default_factory=list) 

529 current_hooks: list[str] = field(default_factory=list) 

530 current_skills: list[str] = field(default_factory=list) 

531 current_agents: list[str] = field(default_factory=list) 

532 coverage_score: float = 0.0 # 0-1, how well config covers common needs 

533 

534 def to_dict(self) -> dict[str, Any]: 

535 """Convert to dictionary for serialization.""" 

536 return { 

537 "gaps": [g.to_dict() for g in self.gaps], 

538 "current_hooks": self.current_hooks, 

539 "current_skills": self.current_skills, 

540 "current_agents": self.current_agents, 

541 "coverage_score": round(self.coverage_score, 2), 

542 } 

543 

544 

545@dataclass 

546class AgentOutcome: 

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

548 

549 agent_name: str 

550 issue_type: str 

551 success_count: int = 0 

552 failure_count: int = 0 

553 rejection_count: int = 0 

554 

555 @property 

556 def total_count(self) -> int: 

557 """Total issues handled.""" 

558 return self.success_count + self.failure_count + self.rejection_count 

559 

560 @property 

561 def success_rate(self) -> float: 

562 """Calculate success rate.""" 

563 if self.total_count == 0: 

564 return 0.0 

565 return self.success_count / self.total_count 

566 

567 def to_dict(self) -> dict[str, Any]: 

568 """Convert to dictionary for serialization.""" 

569 return { 

570 "agent_name": self.agent_name, 

571 "issue_type": self.issue_type, 

572 "success_count": self.success_count, 

573 "failure_count": self.failure_count, 

574 "rejection_count": self.rejection_count, 

575 "total_count": self.total_count, 

576 "success_rate": round(self.success_rate, 3), 

577 } 

578 

579 

580@dataclass 

581class AgentEffectivenessAnalysis: 

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

583 

584 outcomes: list[AgentOutcome] = field(default_factory=list) 

585 best_agent_by_type: dict[str, str] = field(default_factory=dict) 

586 problematic_combinations: list[tuple[str, str, str]] = field(default_factory=list) 

587 

588 def to_dict(self) -> dict[str, Any]: 

589 """Convert to dictionary for serialization.""" 

590 return { 

591 "outcomes": [o.to_dict() for o in self.outcomes], 

592 "best_agent_by_type": self.best_agent_by_type, 

593 "problematic_combinations": self.problematic_combinations[:10], 

594 } 

595 

596 

597@dataclass 

598class TechnicalDebtMetrics: 

599 """Technical debt health indicators.""" 

600 

601 backlog_size: int = 0 # Total open issues 

602 backlog_growth_rate: float = 0.0 # Net issues/week 

603 aging_30_plus: int = 0 # Issues > 30 days old 

604 aging_60_plus: int = 0 # Issues > 60 days old 

605 high_priority_open: int = 0 # P0-P1 open 

606 debt_paydown_ratio: float = 0.0 # maintenance vs features 

607 

608 def to_dict(self) -> dict[str, Any]: 

609 """Convert to dictionary for serialization.""" 

610 return { 

611 "backlog_size": self.backlog_size, 

612 "backlog_growth_rate": round(self.backlog_growth_rate, 2), 

613 "aging_30_plus": self.aging_30_plus, 

614 "aging_60_plus": self.aging_60_plus, 

615 "high_priority_open": self.high_priority_open, 

616 "debt_paydown_ratio": round(self.debt_paydown_ratio, 2), 

617 } 

618 

619 

620@dataclass 

621class ComplexityProxy: 

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

623 

624 path: str 

625 avg_resolution_days: float 

626 median_resolution_days: float 

627 issue_count: int 

628 slowest_issue: tuple[str, float] # (issue_id, days) 

629 complexity_score: float # normalized 0-1 

630 comparison_to_baseline: str # "2.1x baseline", etc. 

631 

632 def to_dict(self) -> dict[str, Any]: 

633 """Convert to dictionary for serialization.""" 

634 return { 

635 "path": self.path, 

636 "avg_resolution_days": round(self.avg_resolution_days, 1), 

637 "median_resolution_days": round(self.median_resolution_days, 1), 

638 "issue_count": self.issue_count, 

639 "slowest_issue": { 

640 "issue_id": self.slowest_issue[0], 

641 "days": round(self.slowest_issue[1], 1), 

642 }, 

643 "complexity_score": round(self.complexity_score, 3), 

644 "comparison_to_baseline": self.comparison_to_baseline, 

645 } 

646 

647 

648@dataclass 

649class ComplexityProxyAnalysis: 

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

651 

652 file_complexity: list[ComplexityProxy] = field(default_factory=list) 

653 directory_complexity: list[ComplexityProxy] = field(default_factory=list) 

654 baseline_days: float = 0.0 # median across all issues 

655 complexity_outliers: list[str] = field(default_factory=list) # files >2x baseline 

656 

657 def to_dict(self) -> dict[str, Any]: 

658 """Convert to dictionary for serialization.""" 

659 return { 

660 "file_complexity": [c.to_dict() for c in self.file_complexity[:10]], 

661 "directory_complexity": [c.to_dict() for c in self.directory_complexity[:10]], 

662 "baseline_days": round(self.baseline_days, 1), 

663 "complexity_outliers": self.complexity_outliers[:10], 

664 } 

665 

666 

667@dataclass 

668class CrossCuttingSmell: 

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

670 

671 concern_type: str # "logging", "error-handling", "validation", "auth", "caching" 

672 affected_directories: list[str] = field(default_factory=list) 

673 issue_count: int = 0 

674 issue_ids: list[str] = field(default_factory=list) 

675 scatter_score: float = 0.0 # higher = more scattered (0-1) 

676 suggested_pattern: str = "" # "middleware", "decorator", "aspect" 

677 

678 def to_dict(self) -> dict[str, Any]: 

679 """Convert to dictionary for serialization.""" 

680 return { 

681 "concern_type": self.concern_type, 

682 "affected_directories": self.affected_directories[:10], 

683 "issue_count": self.issue_count, 

684 "issue_ids": self.issue_ids[:10], 

685 "scatter_score": round(self.scatter_score, 2), 

686 "suggested_pattern": self.suggested_pattern, 

687 } 

688 

689 

690@dataclass 

691class CrossCuttingAnalysis: 

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

693 

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

695 most_scattered_concern: str = "" 

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

697 

698 def to_dict(self) -> dict[str, Any]: 

699 """Convert to dictionary for serialization.""" 

700 return { 

701 "smells": [s.to_dict() for s in self.smells], 

702 "most_scattered_concern": self.most_scattered_concern, 

703 "consolidation_opportunities": self.consolidation_opportunities[:10], 

704 } 

705 

706 

707@dataclass 

708class HistoryAnalysis: 

709 """Complete history analysis report.""" 

710 

711 generated_date: date 

712 total_completed: int 

713 total_active: int 

714 date_range_start: date | None 

715 date_range_end: date | None 

716 

717 # Core summary (from existing HistorySummary) 

718 summary: HistorySummary 

719 

720 # Trend analysis 

721 period_metrics: list[PeriodMetrics] = field(default_factory=list) 

722 velocity_trend: str = "stable" # "increasing", "stable", "decreasing" 

723 bug_ratio_trend: str = "stable" 

724 

725 # Subsystem health 

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

727 

728 # Hotspot analysis 

729 hotspot_analysis: HotspotAnalysis | None = None 

730 

731 # Coupling analysis 

732 coupling_analysis: CouplingAnalysis | None = None 

733 

734 # Regression clustering analysis 

735 regression_analysis: RegressionAnalysis | None = None 

736 

737 # Test gap analysis 

738 test_gap_analysis: TestGapAnalysis | None = None 

739 

740 # Rejection analysis 

741 rejection_analysis: RejectionAnalysis | None = None 

742 

743 # Manual pattern analysis 

744 manual_pattern_analysis: ManualPatternAnalysis | None = None 

745 

746 # Agent effectiveness analysis 

747 agent_effectiveness_analysis: AgentEffectivenessAnalysis | None = None 

748 

749 # Complexity proxy analysis 

750 complexity_proxy_analysis: ComplexityProxyAnalysis | None = None 

751 

752 # Configuration gaps analysis 

753 config_gaps_analysis: ConfigGapsAnalysis | None = None 

754 

755 # Cross-cutting concern analysis 

756 cross_cutting_analysis: CrossCuttingAnalysis | None = None 

757 

758 # Evolution trigger analysis (ENH-1911) 

759 recurring_feedback_analysis: RecurringFeedbackAnalysis | None = None 

760 skill_bypass_analysis: SkillBypassAnalysis | None = None 

761 

762 # Technical debt 

763 debt_metrics: TechnicalDebtMetrics | None = None 

764 

765 # Comparative analysis (optional) 

766 comparison_period: str | None = None # e.g., "30d" 

767 previous_period: PeriodMetrics | None = None 

768 current_period: PeriodMetrics | None = None 

769 

770 def to_dict(self) -> dict[str, Any]: 

771 """Convert to dictionary for serialization.""" 

772 return { 

773 "generated_date": self.generated_date.isoformat(), 

774 "total_completed": self.total_completed, 

775 "total_active": self.total_active, 

776 "date_range_start": ( 

777 self.date_range_start.isoformat() if self.date_range_start else None 

778 ), 

779 "date_range_end": (self.date_range_end.isoformat() if self.date_range_end else None), 

780 "summary": self.summary.to_dict(), 

781 "period_metrics": [p.to_dict() for p in self.period_metrics], 

782 "velocity_trend": self.velocity_trend, 

783 "bug_ratio_trend": self.bug_ratio_trend, 

784 "subsystem_health": [s.to_dict() for s in self.subsystem_health], 

785 "hotspot_analysis": ( 

786 self.hotspot_analysis.to_dict() if self.hotspot_analysis else None 

787 ), 

788 "coupling_analysis": ( 

789 self.coupling_analysis.to_dict() if self.coupling_analysis else None 

790 ), 

791 "regression_analysis": ( 

792 self.regression_analysis.to_dict() if self.regression_analysis else None 

793 ), 

794 "test_gap_analysis": ( 

795 self.test_gap_analysis.to_dict() if self.test_gap_analysis else None 

796 ), 

797 "rejection_analysis": ( 

798 self.rejection_analysis.to_dict() if self.rejection_analysis else None 

799 ), 

800 "manual_pattern_analysis": ( 

801 self.manual_pattern_analysis.to_dict() if self.manual_pattern_analysis else None 

802 ), 

803 "agent_effectiveness_analysis": ( 

804 self.agent_effectiveness_analysis.to_dict() 

805 if self.agent_effectiveness_analysis 

806 else None 

807 ), 

808 "complexity_proxy_analysis": ( 

809 self.complexity_proxy_analysis.to_dict() if self.complexity_proxy_analysis else None 

810 ), 

811 "config_gaps_analysis": ( 

812 self.config_gaps_analysis.to_dict() if self.config_gaps_analysis else None 

813 ), 

814 "cross_cutting_analysis": ( 

815 self.cross_cutting_analysis.to_dict() if self.cross_cutting_analysis else None 

816 ), 

817 "recurring_feedback_analysis": ( 

818 self.recurring_feedback_analysis.to_dict() 

819 if self.recurring_feedback_analysis 

820 else None 

821 ), 

822 "skill_bypass_analysis": ( 

823 self.skill_bypass_analysis.to_dict() if self.skill_bypass_analysis else None 

824 ), 

825 "debt_metrics": self.debt_metrics.to_dict() if self.debt_metrics else None, 

826 "comparison_period": self.comparison_period, 

827 "previous_period": (self.previous_period.to_dict() if self.previous_period else None), 

828 "current_period": (self.current_period.to_dict() if self.current_period else None), 

829 }