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, datetime

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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