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
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-16 13:08 -0500
1"""Issue history data models.
3Dataclasses for issue history analysis including completed issues,
4summary statistics, hotspot detection, coupling analysis, regression
5clustering, test gap analysis, and technical debt metrics.
6"""
8from __future__ import annotations
10from dataclasses import dataclass, field
11from datetime import date, datetime
12from pathlib import Path
13from typing import Any
16@dataclass
17class CompletedIssue:
18 """Parsed information from a completed issue file."""
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
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 }
45@dataclass
46class HistorySummary:
47 """Summary statistics for completed issues."""
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
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
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
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 }
84@dataclass
85class PeriodMetrics:
86 """Metrics for a specific time period."""
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
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
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 }
120@dataclass
121class SubsystemHealth:
122 """Health metrics for a subsystem (directory)."""
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"
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 }
141@dataclass
142class Hotspot:
143 """A file or directory that appears in multiple issues."""
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"
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 }
164@dataclass
165class HotspotAnalysis:
166 """Analysis of files and directories appearing repeatedly in issues."""
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
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 }
181@dataclass
182class CouplingPair:
183 """A pair of files that frequently appear together in issues."""
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)
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 }
202@dataclass
203class CouplingAnalysis:
204 """Analysis of files that frequently change together."""
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
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 }
219@dataclass
220class RegressionCluster:
221 """A cluster of bugs where fixes led to new bugs."""
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"
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 }
242@dataclass
243class RegressionAnalysis:
244 """Analysis of regression patterns in bug fixes."""
246 clusters: list[RegressionCluster] = field(default_factory=list)
247 total_regression_chains: int = 0
248 most_fragile_files: list[str] = field(default_factory=list)
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 }
259@dataclass
260class TestGap:
261 """A source file with bugs but missing or weak test coverage."""
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"
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 }
284@dataclass
285class TestGapAnalysis:
286 """Analysis of test coverage gaps correlated with bug occurrences."""
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)
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 }
305@dataclass
306class RejectionMetrics:
307 """Metrics for rejection and invalid closure tracking."""
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
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
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
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 }
344@dataclass
345class RejectionAnalysis:
346 """Analysis of rejection and invalid closure patterns."""
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"
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 }
365@dataclass
366class ManualPattern:
367 """A recurring manual activity detected across issues."""
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"
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 }
390@dataclass
391class ManualPatternAnalysis:
392 """Analysis of recurring manual activities that could be automated."""
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)
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
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 }
417@dataclass
418class RecurringFeedback:
419 """A user correction that has recurred across multiple sessions."""
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)
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 }
440@dataclass
441class RecurringFeedbackAnalysis:
442 """Analysis of recurring user corrections that could become permanent rules."""
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)
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 }
461@dataclass
462class SkillBypass:
463 """A skill that the user repeatedly performed manually instead of invoking."""
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
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 }
482@dataclass
483class SkillBypassAnalysis:
484 """Analysis of skills users bypassed by doing the work manually."""
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
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 }
501@dataclass
502class ConfigGap:
503 """A gap in configuration that could address recurring manual work."""
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
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 }
524@dataclass
525class ConfigGapsAnalysis:
526 """Analysis of configuration gaps based on manual pattern detection."""
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
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 }
545@dataclass
546class AgentOutcome:
547 """Metrics for a single agent processing a specific issue type."""
549 agent_name: str
550 issue_type: str
551 success_count: int = 0
552 failure_count: int = 0
553 rejection_count: int = 0
555 @property
556 def total_count(self) -> int:
557 """Total issues handled."""
558 return self.success_count + self.failure_count + self.rejection_count
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
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 }
580@dataclass
581class AgentEffectivenessAnalysis:
582 """Analysis of agent effectiveness across issue types."""
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)
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 }
597@dataclass
598class TechnicalDebtMetrics:
599 """Technical debt health indicators."""
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
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 }
620@dataclass
621class ComplexityProxy:
622 """Duration-based complexity proxy for a file or directory."""
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.
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 }
648@dataclass
649class ComplexityProxyAnalysis:
650 """Analysis using issue duration as complexity proxy."""
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
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 }
667@dataclass
668class CrossCuttingSmell:
669 """A detected cross-cutting concern scattered across the codebase."""
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"
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 }
690@dataclass
691class CrossCuttingAnalysis:
692 """Analysis of cross-cutting concerns scattered across the codebase."""
694 smells: list[CrossCuttingSmell] = field(default_factory=list)
695 most_scattered_concern: str = ""
696 consolidation_opportunities: list[str] = field(default_factory=list)
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 }
707@dataclass
708class HistoryAnalysis:
709 """Complete history analysis report."""
711 generated_date: date
712 total_completed: int
713 total_active: int
714 date_range_start: date | None
715 date_range_end: date | None
717 # Core summary (from existing HistorySummary)
718 summary: HistorySummary
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"
725 # Subsystem health
726 subsystem_health: list[SubsystemHealth] = field(default_factory=list)
728 # Hotspot analysis
729 hotspot_analysis: HotspotAnalysis | None = None
731 # Coupling analysis
732 coupling_analysis: CouplingAnalysis | None = None
734 # Regression clustering analysis
735 regression_analysis: RegressionAnalysis | None = None
737 # Test gap analysis
738 test_gap_analysis: TestGapAnalysis | None = None
740 # Rejection analysis
741 rejection_analysis: RejectionAnalysis | None = None
743 # Manual pattern analysis
744 manual_pattern_analysis: ManualPatternAnalysis | None = None
746 # Agent effectiveness analysis
747 agent_effectiveness_analysis: AgentEffectivenessAnalysis | None = None
749 # Complexity proxy analysis
750 complexity_proxy_analysis: ComplexityProxyAnalysis | None = None
752 # Configuration gaps analysis
753 config_gaps_analysis: ConfigGapsAnalysis | None = None
755 # Cross-cutting concern analysis
756 cross_cutting_analysis: CrossCuttingAnalysis | None = None
758 # Evolution trigger analysis (ENH-1911)
759 recurring_feedback_analysis: RecurringFeedbackAnalysis | None = None
760 skill_bypass_analysis: SkillBypassAnalysis | None = None
762 # Technical debt
763 debt_metrics: TechnicalDebtMetrics | None = None
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
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 }