doit-toolkit-cli 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doit_cli/__init__.py +1356 -0
- doit_cli/cli/__init__.py +26 -0
- doit_cli/cli/analytics_command.py +616 -0
- doit_cli/cli/context_command.py +213 -0
- doit_cli/cli/diagram_command.py +304 -0
- doit_cli/cli/fixit_command.py +641 -0
- doit_cli/cli/hooks_command.py +211 -0
- doit_cli/cli/init_command.py +613 -0
- doit_cli/cli/memory_command.py +293 -0
- doit_cli/cli/status_command.py +117 -0
- doit_cli/cli/sync_prompts_command.py +248 -0
- doit_cli/cli/validate_command.py +196 -0
- doit_cli/cli/verify_command.py +204 -0
- doit_cli/cli/workflow_mixin.py +224 -0
- doit_cli/cli/xref_command.py +555 -0
- doit_cli/formatters/__init__.py +8 -0
- doit_cli/formatters/base.py +38 -0
- doit_cli/formatters/json_formatter.py +126 -0
- doit_cli/formatters/markdown_formatter.py +97 -0
- doit_cli/formatters/rich_formatter.py +257 -0
- doit_cli/main.py +49 -0
- doit_cli/models/__init__.py +139 -0
- doit_cli/models/agent.py +74 -0
- doit_cli/models/analytics_models.py +384 -0
- doit_cli/models/context_config.py +464 -0
- doit_cli/models/crossref_models.py +182 -0
- doit_cli/models/diagram_models.py +363 -0
- doit_cli/models/fixit_models.py +355 -0
- doit_cli/models/hook_config.py +125 -0
- doit_cli/models/project.py +91 -0
- doit_cli/models/results.py +121 -0
- doit_cli/models/search_models.py +228 -0
- doit_cli/models/status_models.py +195 -0
- doit_cli/models/sync_models.py +146 -0
- doit_cli/models/template.py +77 -0
- doit_cli/models/validation_models.py +175 -0
- doit_cli/models/workflow_models.py +319 -0
- doit_cli/prompts/__init__.py +5 -0
- doit_cli/prompts/fixit_prompts.py +344 -0
- doit_cli/prompts/interactive.py +390 -0
- doit_cli/rules/__init__.py +5 -0
- doit_cli/rules/builtin_rules.py +160 -0
- doit_cli/services/__init__.py +79 -0
- doit_cli/services/agent_detector.py +168 -0
- doit_cli/services/analytics_service.py +218 -0
- doit_cli/services/architecture_generator.py +290 -0
- doit_cli/services/backup_service.py +204 -0
- doit_cli/services/config_loader.py +113 -0
- doit_cli/services/context_loader.py +1121 -0
- doit_cli/services/coverage_calculator.py +142 -0
- doit_cli/services/crossref_service.py +237 -0
- doit_cli/services/cycle_time_calculator.py +134 -0
- doit_cli/services/date_inferrer.py +349 -0
- doit_cli/services/diagram_service.py +337 -0
- doit_cli/services/drift_detector.py +109 -0
- doit_cli/services/entity_parser.py +301 -0
- doit_cli/services/er_diagram_generator.py +197 -0
- doit_cli/services/fixit_service.py +699 -0
- doit_cli/services/github_service.py +192 -0
- doit_cli/services/hook_manager.py +258 -0
- doit_cli/services/hook_validator.py +528 -0
- doit_cli/services/input_validator.py +322 -0
- doit_cli/services/memory_search.py +527 -0
- doit_cli/services/mermaid_validator.py +334 -0
- doit_cli/services/prompt_transformer.py +91 -0
- doit_cli/services/prompt_writer.py +133 -0
- doit_cli/services/query_interpreter.py +428 -0
- doit_cli/services/report_exporter.py +219 -0
- doit_cli/services/report_generator.py +256 -0
- doit_cli/services/requirement_parser.py +112 -0
- doit_cli/services/roadmap_summarizer.py +209 -0
- doit_cli/services/rule_engine.py +443 -0
- doit_cli/services/scaffolder.py +215 -0
- doit_cli/services/score_calculator.py +172 -0
- doit_cli/services/section_parser.py +204 -0
- doit_cli/services/spec_scanner.py +327 -0
- doit_cli/services/state_manager.py +355 -0
- doit_cli/services/status_reporter.py +143 -0
- doit_cli/services/task_parser.py +347 -0
- doit_cli/services/template_manager.py +710 -0
- doit_cli/services/template_reader.py +158 -0
- doit_cli/services/user_journey_generator.py +214 -0
- doit_cli/services/user_story_parser.py +232 -0
- doit_cli/services/validation_service.py +188 -0
- doit_cli/services/validator.py +232 -0
- doit_cli/services/velocity_tracker.py +173 -0
- doit_cli/services/workflow_engine.py +405 -0
- doit_cli/templates/agent-file-template.md +28 -0
- doit_cli/templates/checklist-template.md +39 -0
- doit_cli/templates/commands/doit.checkin.md +363 -0
- doit_cli/templates/commands/doit.constitution.md +187 -0
- doit_cli/templates/commands/doit.documentit.md +485 -0
- doit_cli/templates/commands/doit.fixit.md +181 -0
- doit_cli/templates/commands/doit.implementit.md +265 -0
- doit_cli/templates/commands/doit.planit.md +262 -0
- doit_cli/templates/commands/doit.reviewit.md +355 -0
- doit_cli/templates/commands/doit.roadmapit.md +368 -0
- doit_cli/templates/commands/doit.scaffoldit.md +458 -0
- doit_cli/templates/commands/doit.specit.md +521 -0
- doit_cli/templates/commands/doit.taskit.md +304 -0
- doit_cli/templates/commands/doit.testit.md +277 -0
- doit_cli/templates/config/context.yaml +134 -0
- doit_cli/templates/config/hooks.yaml +93 -0
- doit_cli/templates/config/validation-rules.yaml +64 -0
- doit_cli/templates/github-issue-templates/epic.yml +78 -0
- doit_cli/templates/github-issue-templates/feature.yml +116 -0
- doit_cli/templates/github-issue-templates/task.yml +129 -0
- doit_cli/templates/hooks/.gitkeep +0 -0
- doit_cli/templates/hooks/post-commit.sh +25 -0
- doit_cli/templates/hooks/post-merge.sh +75 -0
- doit_cli/templates/hooks/pre-commit.sh +17 -0
- doit_cli/templates/hooks/pre-push.sh +18 -0
- doit_cli/templates/memory/completed_roadmap.md +50 -0
- doit_cli/templates/memory/constitution.md +125 -0
- doit_cli/templates/memory/roadmap.md +61 -0
- doit_cli/templates/plan-template.md +146 -0
- doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
- doit_cli/templates/scripts/bash/common.sh +156 -0
- doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
- doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
- doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
- doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
- doit_cli/templates/scripts/powershell/common.ps1 +137 -0
- doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
- doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
- doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
- doit_cli/templates/spec-template.md +159 -0
- doit_cli/templates/tasks-template.md +313 -0
- doit_cli/templates/vscode-settings.json +14 -0
- doit_toolkit_cli-0.1.9.dist-info/METADATA +324 -0
- doit_toolkit_cli-0.1.9.dist-info/RECORD +134 -0
- doit_toolkit_cli-0.1.9.dist-info/WHEEL +4 -0
- doit_toolkit_cli-0.1.9.dist-info/entry_points.txt +2 -0
- doit_toolkit_cli-0.1.9.dist-info/licenses/LICENSE +21 -0
doit_cli/models/agent.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Agent enum and configuration for supported AI coding assistants."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Agent(str, Enum):
|
|
7
|
+
"""Supported AI coding agents."""
|
|
8
|
+
|
|
9
|
+
CLAUDE = "claude"
|
|
10
|
+
COPILOT = "copilot"
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def display_name(self) -> str:
|
|
14
|
+
"""Human-readable name for display."""
|
|
15
|
+
names = {
|
|
16
|
+
Agent.CLAUDE: "Claude Code",
|
|
17
|
+
Agent.COPILOT: "GitHub Copilot",
|
|
18
|
+
}
|
|
19
|
+
return names[self]
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def command_directory(self) -> str:
|
|
23
|
+
"""Relative path to command/prompt directory."""
|
|
24
|
+
directories = {
|
|
25
|
+
Agent.CLAUDE: ".claude/commands",
|
|
26
|
+
Agent.COPILOT: ".github/prompts",
|
|
27
|
+
}
|
|
28
|
+
return directories[self]
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def template_directory(self) -> str:
|
|
32
|
+
"""Relative path within bundled templates.
|
|
33
|
+
|
|
34
|
+
All agents now use commands/ as the single source of truth.
|
|
35
|
+
Copilot prompts are generated dynamically via transformation.
|
|
36
|
+
"""
|
|
37
|
+
return "commands"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def needs_transformation(self) -> bool:
|
|
41
|
+
"""Whether templates need transformation for this agent.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True for Copilot (requires transformation from command format),
|
|
45
|
+
False for Claude (direct copy).
|
|
46
|
+
"""
|
|
47
|
+
return self == Agent.COPILOT
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def file_extension(self) -> str:
|
|
51
|
+
"""File extension for command files."""
|
|
52
|
+
extensions = {
|
|
53
|
+
Agent.CLAUDE: ".md",
|
|
54
|
+
Agent.COPILOT: ".prompt.md",
|
|
55
|
+
}
|
|
56
|
+
return extensions[self]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def file_pattern(self) -> str:
|
|
60
|
+
"""Glob pattern for doit-managed files."""
|
|
61
|
+
patterns = {
|
|
62
|
+
Agent.CLAUDE: "doit.*.md",
|
|
63
|
+
Agent.COPILOT: "doit.*.prompt.md",
|
|
64
|
+
}
|
|
65
|
+
return patterns[self]
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def file_prefix(self) -> str:
|
|
69
|
+
"""Filename prefix for doit commands."""
|
|
70
|
+
prefixes = {
|
|
71
|
+
Agent.CLAUDE: "doit.",
|
|
72
|
+
Agent.COPILOT: "doit.",
|
|
73
|
+
}
|
|
74
|
+
return prefixes[self]
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Models for spec analytics and metrics dashboard.
|
|
2
|
+
|
|
3
|
+
This module provides dataclasses for analytics data:
|
|
4
|
+
- SpecMetadata: Extended spec info with lifecycle dates
|
|
5
|
+
- CycleTimeRecord: Individual cycle time for completed specs
|
|
6
|
+
- CycleTimeStats: Statistical summary of cycle times
|
|
7
|
+
- VelocityDataPoint: Weekly velocity aggregation
|
|
8
|
+
- AnalyticsReport: Complete analytics report
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import date, datetime, timedelta
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from statistics import mean, median, stdev
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from .status_models import SpecState, SpecStatus
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SpecMetadata:
|
|
22
|
+
"""Extended spec metadata with lifecycle dates.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
name: Spec directory name (e.g., "036-spec-analytics-dashboard")
|
|
26
|
+
status: Current spec state (Draft, In Progress, Complete, Approved)
|
|
27
|
+
created_at: Date spec was created (from metadata or git)
|
|
28
|
+
completed_at: Date spec reached Complete status (None if not complete)
|
|
29
|
+
current_phase: Human-readable phase description
|
|
30
|
+
days_in_progress: Days since creation (for in-progress specs)
|
|
31
|
+
path: Path to the spec.md file
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
status: SpecState
|
|
36
|
+
created_at: Optional[date]
|
|
37
|
+
completed_at: Optional[date]
|
|
38
|
+
current_phase: str = ""
|
|
39
|
+
days_in_progress: int = 0
|
|
40
|
+
path: Optional[Path] = None
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_spec_status(
|
|
44
|
+
cls,
|
|
45
|
+
spec_status: SpecStatus,
|
|
46
|
+
created_at: Optional[date],
|
|
47
|
+
completed_at: Optional[date],
|
|
48
|
+
) -> "SpecMetadata":
|
|
49
|
+
"""Create from existing SpecStatus with added dates.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
spec_status: The base SpecStatus object
|
|
53
|
+
created_at: Inferred creation date
|
|
54
|
+
completed_at: Inferred completion date
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
SpecMetadata with enriched date information
|
|
58
|
+
"""
|
|
59
|
+
days = 0
|
|
60
|
+
if created_at and not completed_at:
|
|
61
|
+
days = (date.today() - created_at).days
|
|
62
|
+
|
|
63
|
+
phase = cls._determine_phase(spec_status.status)
|
|
64
|
+
|
|
65
|
+
return cls(
|
|
66
|
+
name=spec_status.name,
|
|
67
|
+
status=spec_status.status,
|
|
68
|
+
created_at=created_at,
|
|
69
|
+
completed_at=completed_at,
|
|
70
|
+
current_phase=phase,
|
|
71
|
+
days_in_progress=days,
|
|
72
|
+
path=spec_status.path,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _determine_phase(status: SpecState) -> str:
|
|
77
|
+
"""Map status to human-readable phase.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
status: The SpecState enum value
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Human-readable phase name
|
|
84
|
+
"""
|
|
85
|
+
phases = {
|
|
86
|
+
SpecState.DRAFT: "Specification",
|
|
87
|
+
SpecState.IN_PROGRESS: "Implementation",
|
|
88
|
+
SpecState.COMPLETE: "Review",
|
|
89
|
+
SpecState.APPROVED: "Done",
|
|
90
|
+
SpecState.ERROR: "Unknown",
|
|
91
|
+
}
|
|
92
|
+
return phases.get(status, "Unknown")
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def is_completed(self) -> bool:
|
|
96
|
+
"""Check if spec is in a completed state."""
|
|
97
|
+
return self.status in (SpecState.COMPLETE, SpecState.APPROVED)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def cycle_time_days(self) -> Optional[int]:
|
|
101
|
+
"""Calculate cycle time in days if completed."""
|
|
102
|
+
if self.created_at and self.completed_at:
|
|
103
|
+
return (self.completed_at - self.created_at).days
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class CycleTimeRecord:
|
|
109
|
+
"""Cycle time for a single completed spec.
|
|
110
|
+
|
|
111
|
+
Attributes:
|
|
112
|
+
feature_name: Spec name (foreign key to SpecMetadata)
|
|
113
|
+
days_to_complete: Total days from creation to completion
|
|
114
|
+
start_date: Creation date
|
|
115
|
+
end_date: Completion date
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
feature_name: str
|
|
119
|
+
days_to_complete: int
|
|
120
|
+
start_date: date
|
|
121
|
+
end_date: date
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_metadata(cls, metadata: SpecMetadata) -> Optional["CycleTimeRecord"]:
|
|
125
|
+
"""Create from SpecMetadata if spec is complete.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
metadata: SpecMetadata with date information
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
CycleTimeRecord if spec has both dates, None otherwise
|
|
132
|
+
"""
|
|
133
|
+
if not metadata.created_at or not metadata.completed_at:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
days = (metadata.completed_at - metadata.created_at).days
|
|
137
|
+
return cls(
|
|
138
|
+
feature_name=metadata.name,
|
|
139
|
+
days_to_complete=max(days, 0), # Handle negative edge case
|
|
140
|
+
start_date=metadata.created_at,
|
|
141
|
+
end_date=metadata.completed_at,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class CycleTimeStats:
|
|
147
|
+
"""Statistical summary of cycle times.
|
|
148
|
+
|
|
149
|
+
Attributes:
|
|
150
|
+
average_days: Mean cycle time
|
|
151
|
+
median_days: Median cycle time
|
|
152
|
+
min_days: Shortest cycle time
|
|
153
|
+
max_days: Longest cycle time
|
|
154
|
+
std_dev_days: Standard deviation
|
|
155
|
+
sample_count: Number of completed specs in calculation
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
average_days: float
|
|
159
|
+
median_days: float
|
|
160
|
+
min_days: int
|
|
161
|
+
max_days: int
|
|
162
|
+
std_dev_days: float
|
|
163
|
+
sample_count: int
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def calculate(cls, records: list[CycleTimeRecord]) -> Optional["CycleTimeStats"]:
|
|
167
|
+
"""Calculate statistics from cycle time records.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
records: List of CycleTimeRecord objects
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
CycleTimeStats if records provided, None if empty
|
|
174
|
+
"""
|
|
175
|
+
if not records:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
days = [r.days_to_complete for r in records]
|
|
179
|
+
|
|
180
|
+
return cls(
|
|
181
|
+
average_days=round(mean(days), 1),
|
|
182
|
+
median_days=round(median(days), 1),
|
|
183
|
+
min_days=min(days),
|
|
184
|
+
max_days=max(days),
|
|
185
|
+
std_dev_days=round(stdev(days), 1) if len(days) > 1 else 0.0,
|
|
186
|
+
sample_count=len(days),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass
|
|
191
|
+
class VelocityDataPoint:
|
|
192
|
+
"""Velocity data for a single week.
|
|
193
|
+
|
|
194
|
+
Attributes:
|
|
195
|
+
week_key: ISO week identifier (e.g., "2026-W03")
|
|
196
|
+
week_start: Monday of the week
|
|
197
|
+
specs_completed: Number of specs completed this week
|
|
198
|
+
spec_names: Names of specs completed this week
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
week_key: str
|
|
202
|
+
week_start: date
|
|
203
|
+
specs_completed: int
|
|
204
|
+
spec_names: list[str] = field(default_factory=list)
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_completion(cls, completion_date: date, spec_name: str) -> "VelocityDataPoint":
|
|
208
|
+
"""Create a velocity point from a single completion.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
completion_date: Date the spec was completed
|
|
212
|
+
spec_name: Name of the completed spec
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
VelocityDataPoint for the week of the completion
|
|
216
|
+
"""
|
|
217
|
+
year, week, _ = completion_date.isocalendar()
|
|
218
|
+
week_key = f"{year}-W{week:02d}"
|
|
219
|
+
|
|
220
|
+
# Calculate Monday of this ISO week
|
|
221
|
+
monday = completion_date - timedelta(days=completion_date.weekday())
|
|
222
|
+
|
|
223
|
+
return cls(
|
|
224
|
+
week_key=week_key,
|
|
225
|
+
week_start=monday,
|
|
226
|
+
specs_completed=1,
|
|
227
|
+
spec_names=[spec_name],
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def merge(self, other: "VelocityDataPoint") -> "VelocityDataPoint":
|
|
231
|
+
"""Merge another data point for the same week.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
other: Another VelocityDataPoint for the same week
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Merged VelocityDataPoint with combined counts
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
ValueError: If weeks don't match
|
|
241
|
+
"""
|
|
242
|
+
if self.week_key != other.week_key:
|
|
243
|
+
raise ValueError("Cannot merge different weeks")
|
|
244
|
+
|
|
245
|
+
return VelocityDataPoint(
|
|
246
|
+
week_key=self.week_key,
|
|
247
|
+
week_start=self.week_start,
|
|
248
|
+
specs_completed=self.specs_completed + other.specs_completed,
|
|
249
|
+
spec_names=self.spec_names + other.spec_names,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class AnalyticsReport:
|
|
255
|
+
"""Complete analytics report.
|
|
256
|
+
|
|
257
|
+
Attributes:
|
|
258
|
+
report_id: Unique identifier (timestamp-based)
|
|
259
|
+
generated_at: Report generation timestamp
|
|
260
|
+
project_root: Project directory path
|
|
261
|
+
specs: All spec metadata
|
|
262
|
+
total_specs: Total spec count
|
|
263
|
+
completion_pct: Percentage of completed/approved specs
|
|
264
|
+
by_status: Counts grouped by status
|
|
265
|
+
cycle_stats: Cycle time statistics (None if no completions)
|
|
266
|
+
velocity: Weekly velocity data points
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
report_id: str
|
|
270
|
+
generated_at: datetime
|
|
271
|
+
project_root: Path
|
|
272
|
+
specs: list[SpecMetadata]
|
|
273
|
+
total_specs: int
|
|
274
|
+
completion_pct: float
|
|
275
|
+
by_status: dict[SpecState, int]
|
|
276
|
+
cycle_stats: Optional[CycleTimeStats]
|
|
277
|
+
velocity: list[VelocityDataPoint]
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def generate(
|
|
281
|
+
cls,
|
|
282
|
+
specs: list[SpecMetadata],
|
|
283
|
+
project_root: Path,
|
|
284
|
+
) -> "AnalyticsReport":
|
|
285
|
+
"""Generate a complete analytics report.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
specs: List of SpecMetadata objects
|
|
289
|
+
project_root: Root path of the project
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Complete AnalyticsReport with all calculated metrics
|
|
293
|
+
"""
|
|
294
|
+
now = datetime.now()
|
|
295
|
+
report_id = now.strftime("%Y%m%d-%H%M%S")
|
|
296
|
+
|
|
297
|
+
# Calculate completion percentage
|
|
298
|
+
completed = sum(
|
|
299
|
+
1 for s in specs if s.status in (SpecState.COMPLETE, SpecState.APPROVED)
|
|
300
|
+
)
|
|
301
|
+
pct = (completed / len(specs) * 100) if specs else 0.0
|
|
302
|
+
|
|
303
|
+
# Group by status
|
|
304
|
+
by_status: dict[SpecState, int] = {}
|
|
305
|
+
for spec in specs:
|
|
306
|
+
by_status[spec.status] = by_status.get(spec.status, 0) + 1
|
|
307
|
+
|
|
308
|
+
# Calculate cycle times
|
|
309
|
+
records = [CycleTimeRecord.from_metadata(s) for s in specs]
|
|
310
|
+
records = [r for r in records if r is not None]
|
|
311
|
+
cycle_stats = CycleTimeStats.calculate(records)
|
|
312
|
+
|
|
313
|
+
# Calculate velocity
|
|
314
|
+
velocity = cls._calculate_velocity(specs)
|
|
315
|
+
|
|
316
|
+
return cls(
|
|
317
|
+
report_id=report_id,
|
|
318
|
+
generated_at=now,
|
|
319
|
+
project_root=project_root,
|
|
320
|
+
specs=specs,
|
|
321
|
+
total_specs=len(specs),
|
|
322
|
+
completion_pct=round(pct, 1),
|
|
323
|
+
by_status=by_status,
|
|
324
|
+
cycle_stats=cycle_stats,
|
|
325
|
+
velocity=velocity,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _calculate_velocity(specs: list[SpecMetadata]) -> list[VelocityDataPoint]:
|
|
330
|
+
"""Aggregate completions by week.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
specs: List of SpecMetadata objects
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
List of VelocityDataPoint sorted by week
|
|
337
|
+
"""
|
|
338
|
+
weekly: dict[str, VelocityDataPoint] = {}
|
|
339
|
+
|
|
340
|
+
for spec in specs:
|
|
341
|
+
if spec.completed_at:
|
|
342
|
+
point = VelocityDataPoint.from_completion(spec.completed_at, spec.name)
|
|
343
|
+
if point.week_key in weekly:
|
|
344
|
+
weekly[point.week_key] = weekly[point.week_key].merge(point)
|
|
345
|
+
else:
|
|
346
|
+
weekly[point.week_key] = point
|
|
347
|
+
|
|
348
|
+
# Sort by week key (descending - most recent first)
|
|
349
|
+
return sorted(weekly.values(), key=lambda v: v.week_key, reverse=True)
|
|
350
|
+
|
|
351
|
+
def to_dict(self) -> dict:
|
|
352
|
+
"""Convert report to dictionary for JSON serialization.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Dictionary representation of the report
|
|
356
|
+
"""
|
|
357
|
+
return {
|
|
358
|
+
"success": True,
|
|
359
|
+
"report_id": self.report_id,
|
|
360
|
+
"generated_at": self.generated_at.isoformat(),
|
|
361
|
+
"data": {
|
|
362
|
+
"total_specs": self.total_specs,
|
|
363
|
+
"completion_pct": self.completion_pct,
|
|
364
|
+
"by_status": {
|
|
365
|
+
state.value: count for state, count in self.by_status.items()
|
|
366
|
+
},
|
|
367
|
+
"cycle_stats": (
|
|
368
|
+
{
|
|
369
|
+
"average_days": self.cycle_stats.average_days,
|
|
370
|
+
"median_days": self.cycle_stats.median_days,
|
|
371
|
+
"min_days": self.cycle_stats.min_days,
|
|
372
|
+
"max_days": self.cycle_stats.max_days,
|
|
373
|
+
"std_dev_days": self.cycle_stats.std_dev_days,
|
|
374
|
+
"sample_count": self.cycle_stats.sample_count,
|
|
375
|
+
}
|
|
376
|
+
if self.cycle_stats
|
|
377
|
+
else None
|
|
378
|
+
),
|
|
379
|
+
"velocity": [
|
|
380
|
+
{"week": v.week_key, "completed": v.specs_completed}
|
|
381
|
+
for v in self.velocity
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
}
|