doit-toolkit-cli 0.1.10__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.
Potentially problematic release.
This version of doit-toolkit-cli might be problematic. Click here for more details.
- 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/roadmapit_command.py +10 -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 +51 -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 +1123 -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 +389 -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.10.dist-info/METADATA +324 -0
- doit_toolkit_cli-0.1.10.dist-info/RECORD +135 -0
- doit_toolkit_cli-0.1.10.dist-info/WHEEL +4 -0
- doit_toolkit_cli-0.1.10.dist-info/entry_points.txt +2 -0
- doit_toolkit_cli-0.1.10.dist-info/licenses/LICENSE +21 -0
doit_cli/cli/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""CLI commands for doit-cli."""
|
|
2
|
+
|
|
3
|
+
from .init_command import init_command, run_init, parse_agent_string
|
|
4
|
+
from .memory_command import memory_app
|
|
5
|
+
from .verify_command import verify_command
|
|
6
|
+
from .workflow_mixin import (
|
|
7
|
+
WorkflowMixin,
|
|
8
|
+
non_interactive_option,
|
|
9
|
+
workflow_command_options,
|
|
10
|
+
validate_required_defaults,
|
|
11
|
+
create_non_interactive_workflow,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"init_command",
|
|
16
|
+
"run_init",
|
|
17
|
+
"parse_agent_string",
|
|
18
|
+
"memory_app",
|
|
19
|
+
"verify_command",
|
|
20
|
+
# Workflow support
|
|
21
|
+
"WorkflowMixin",
|
|
22
|
+
"non_interactive_option",
|
|
23
|
+
"workflow_command_options",
|
|
24
|
+
"validate_required_defaults",
|
|
25
|
+
"create_non_interactive_workflow",
|
|
26
|
+
]
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"""Analytics command for spec metrics and reporting.
|
|
2
|
+
|
|
3
|
+
Provides CLI commands for viewing spec analytics:
|
|
4
|
+
- show: Display completion metrics summary (default)
|
|
5
|
+
- cycles: Display cycle time statistics
|
|
6
|
+
- velocity: Display velocity trends
|
|
7
|
+
- spec: Display individual spec metrics
|
|
8
|
+
- export: Export analytics report
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from datetime import date, datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from ..models.status_models import SpecState
|
|
21
|
+
from ..services.analytics_service import AnalyticsService
|
|
22
|
+
from ..services.spec_scanner import NotADoitProjectError, SpecNotFoundError
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(help="Spec analytics and metrics dashboard")
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_status_emoji(status: SpecState) -> str:
|
|
29
|
+
"""Get emoji for status display."""
|
|
30
|
+
emojis = {
|
|
31
|
+
SpecState.DRAFT: "[dim]📝[/dim]",
|
|
32
|
+
SpecState.IN_PROGRESS: "[yellow]🔄[/yellow]",
|
|
33
|
+
SpecState.COMPLETE: "[green]✅[/green]",
|
|
34
|
+
SpecState.APPROVED: "[cyan]🏆[/cyan]",
|
|
35
|
+
SpecState.ERROR: "[red]❌[/red]",
|
|
36
|
+
}
|
|
37
|
+
return emojis.get(status, "❓")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.callback(invoke_without_command=True)
|
|
41
|
+
def main(ctx: typer.Context):
|
|
42
|
+
"""Spec analytics and metrics dashboard.
|
|
43
|
+
|
|
44
|
+
Run without arguments to show completion metrics summary.
|
|
45
|
+
"""
|
|
46
|
+
if ctx.invoked_subcommand is None:
|
|
47
|
+
# Default to show command
|
|
48
|
+
show(json_output=False)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command()
|
|
52
|
+
def show(
|
|
53
|
+
json_output: bool = typer.Option(
|
|
54
|
+
False, "--json", help="Output as JSON instead of table"
|
|
55
|
+
),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Display completion metrics summary for all specs.
|
|
58
|
+
|
|
59
|
+
Shows total specs, status breakdown, and completion percentage.
|
|
60
|
+
|
|
61
|
+
Exit codes:
|
|
62
|
+
0 - Success
|
|
63
|
+
1 - No specs found
|
|
64
|
+
2 - Not a doit project
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
service = AnalyticsService()
|
|
68
|
+
summary = service.get_completion_summary()
|
|
69
|
+
|
|
70
|
+
if summary["total_specs"] == 0:
|
|
71
|
+
if json_output:
|
|
72
|
+
print(json.dumps({"success": False, "error": "No specs found"}))
|
|
73
|
+
else:
|
|
74
|
+
console.print(
|
|
75
|
+
"[yellow]No specifications found in specs/ directory.[/yellow]"
|
|
76
|
+
)
|
|
77
|
+
raise typer.Exit(code=1)
|
|
78
|
+
|
|
79
|
+
if json_output:
|
|
80
|
+
report = service.generate_report()
|
|
81
|
+
print(json.dumps(report.to_dict(), indent=2))
|
|
82
|
+
else:
|
|
83
|
+
_print_completion_summary(summary)
|
|
84
|
+
|
|
85
|
+
raise typer.Exit(code=0)
|
|
86
|
+
|
|
87
|
+
except NotADoitProjectError as e:
|
|
88
|
+
if json_output:
|
|
89
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
90
|
+
else:
|
|
91
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
92
|
+
raise typer.Exit(code=2)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _print_completion_summary(summary: dict) -> None:
|
|
96
|
+
"""Print completion metrics in Rich tables."""
|
|
97
|
+
console.print()
|
|
98
|
+
console.print("[bold]Spec Analytics[/bold]")
|
|
99
|
+
console.print()
|
|
100
|
+
|
|
101
|
+
# Summary table
|
|
102
|
+
summary_table = Table(title="Summary", show_header=True)
|
|
103
|
+
summary_table.add_column("Metric", style="bold")
|
|
104
|
+
summary_table.add_column("Value", justify="right")
|
|
105
|
+
|
|
106
|
+
summary_table.add_row("Total Specs", str(summary["total_specs"]))
|
|
107
|
+
summary_table.add_row(
|
|
108
|
+
"Completed",
|
|
109
|
+
str(summary["complete_count"] + summary["approved_count"]),
|
|
110
|
+
)
|
|
111
|
+
summary_table.add_row("In Progress", str(summary["in_progress_count"]))
|
|
112
|
+
summary_table.add_row("Draft", str(summary["draft_count"]))
|
|
113
|
+
summary_table.add_row("Completion %", f"{summary['completion_pct']}%")
|
|
114
|
+
|
|
115
|
+
console.print(summary_table)
|
|
116
|
+
console.print()
|
|
117
|
+
|
|
118
|
+
# Status breakdown table
|
|
119
|
+
breakdown_table = Table(title="Status Breakdown", show_header=True)
|
|
120
|
+
breakdown_table.add_column("Status")
|
|
121
|
+
breakdown_table.add_column("Count", justify="right")
|
|
122
|
+
breakdown_table.add_column("Percentage", justify="right")
|
|
123
|
+
|
|
124
|
+
total = summary["total_specs"]
|
|
125
|
+
|
|
126
|
+
status_data = [
|
|
127
|
+
(_get_status_emoji(SpecState.COMPLETE) + " Complete", summary["complete_count"]),
|
|
128
|
+
(_get_status_emoji(SpecState.APPROVED) + " Approved", summary["approved_count"]),
|
|
129
|
+
(_get_status_emoji(SpecState.IN_PROGRESS) + " Progress", summary["in_progress_count"]),
|
|
130
|
+
(_get_status_emoji(SpecState.DRAFT) + " Draft", summary["draft_count"]),
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
for status_name, count in status_data:
|
|
134
|
+
pct = (count / total * 100) if total > 0 else 0
|
|
135
|
+
breakdown_table.add_row(status_name, str(count), f"{pct:.1f}%")
|
|
136
|
+
|
|
137
|
+
console.print(breakdown_table)
|
|
138
|
+
console.print()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command()
|
|
142
|
+
def cycles(
|
|
143
|
+
days: int = typer.Option(30, "--days", "-d", help="Filter to last N days"),
|
|
144
|
+
since: Optional[str] = typer.Option(
|
|
145
|
+
None, "--since", "-s", help="Filter since date (YYYY-MM-DD)"
|
|
146
|
+
),
|
|
147
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Display cycle time statistics for completed specs.
|
|
150
|
+
|
|
151
|
+
Shows average, median, min, max, and standard deviation of cycle times.
|
|
152
|
+
|
|
153
|
+
Exit codes:
|
|
154
|
+
0 - Success
|
|
155
|
+
1 - No completed specs in period
|
|
156
|
+
2 - Not a doit project
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
service = AnalyticsService()
|
|
160
|
+
|
|
161
|
+
# Parse since date if provided
|
|
162
|
+
since_date: Optional[date] = None
|
|
163
|
+
filter_days: Optional[int] = days
|
|
164
|
+
|
|
165
|
+
if since:
|
|
166
|
+
try:
|
|
167
|
+
since_date = datetime.strptime(since, "%Y-%m-%d").date()
|
|
168
|
+
filter_days = None # since overrides days
|
|
169
|
+
except ValueError:
|
|
170
|
+
console.print(
|
|
171
|
+
f"[red]Error:[/red] Invalid date format '{since}'. Use YYYY-MM-DD."
|
|
172
|
+
)
|
|
173
|
+
raise typer.Exit(code=2)
|
|
174
|
+
|
|
175
|
+
stats, records = service.get_cycle_time_stats(days=filter_days, since=since_date)
|
|
176
|
+
|
|
177
|
+
if not records:
|
|
178
|
+
if json_output:
|
|
179
|
+
print(json.dumps({"success": False, "error": "No completed specs in period"}))
|
|
180
|
+
else:
|
|
181
|
+
console.print(
|
|
182
|
+
"[yellow]No completed specs found in the specified period.[/yellow]"
|
|
183
|
+
)
|
|
184
|
+
raise typer.Exit(code=1)
|
|
185
|
+
|
|
186
|
+
if json_output:
|
|
187
|
+
_print_cycles_json(stats, records)
|
|
188
|
+
else:
|
|
189
|
+
_print_cycles_tables(stats, records, days if not since else None, since)
|
|
190
|
+
|
|
191
|
+
raise typer.Exit(code=0)
|
|
192
|
+
|
|
193
|
+
except NotADoitProjectError as e:
|
|
194
|
+
if json_output:
|
|
195
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
196
|
+
else:
|
|
197
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
198
|
+
raise typer.Exit(code=2)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _print_cycles_json(stats, records) -> None:
|
|
202
|
+
"""Print cycle time data as JSON."""
|
|
203
|
+
output = {
|
|
204
|
+
"success": True,
|
|
205
|
+
"cycle_stats": {
|
|
206
|
+
"average_days": stats.average_days,
|
|
207
|
+
"median_days": stats.median_days,
|
|
208
|
+
"min_days": stats.min_days,
|
|
209
|
+
"max_days": stats.max_days,
|
|
210
|
+
"std_dev_days": stats.std_dev_days,
|
|
211
|
+
"sample_count": stats.sample_count,
|
|
212
|
+
},
|
|
213
|
+
"recent_completions": [
|
|
214
|
+
{
|
|
215
|
+
"name": r.feature_name,
|
|
216
|
+
"completed": r.end_date.isoformat(),
|
|
217
|
+
"cycle_days": r.days_to_complete,
|
|
218
|
+
}
|
|
219
|
+
for r in records[:10]
|
|
220
|
+
],
|
|
221
|
+
}
|
|
222
|
+
print(json.dumps(output, indent=2))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _print_cycles_tables(stats, records, days: Optional[int], since: Optional[str]) -> None:
|
|
226
|
+
"""Print cycle time data in Rich tables."""
|
|
227
|
+
console.print()
|
|
228
|
+
|
|
229
|
+
# Title with filter info
|
|
230
|
+
if since:
|
|
231
|
+
title = f"Cycle Time Analysis (since {since})"
|
|
232
|
+
else:
|
|
233
|
+
title = f"Cycle Time Analysis (last {days} days)"
|
|
234
|
+
|
|
235
|
+
console.print(f"[bold]{title}[/bold]")
|
|
236
|
+
console.print()
|
|
237
|
+
|
|
238
|
+
# Statistics table
|
|
239
|
+
stats_table = Table(
|
|
240
|
+
title=f"Statistics (N={stats.sample_count} completed specs)",
|
|
241
|
+
show_header=True,
|
|
242
|
+
)
|
|
243
|
+
stats_table.add_column("Metric", style="bold")
|
|
244
|
+
stats_table.add_column("Value", justify="right")
|
|
245
|
+
|
|
246
|
+
stats_table.add_row("Average", f"{stats.average_days} days")
|
|
247
|
+
stats_table.add_row("Median", f"{stats.median_days} days")
|
|
248
|
+
stats_table.add_row("Minimum", f"{stats.min_days} day{'s' if stats.min_days != 1 else ''}")
|
|
249
|
+
stats_table.add_row("Maximum", f"{stats.max_days} days")
|
|
250
|
+
stats_table.add_row("Std Deviation", f"{stats.std_dev_days} days")
|
|
251
|
+
|
|
252
|
+
console.print(stats_table)
|
|
253
|
+
console.print()
|
|
254
|
+
|
|
255
|
+
# Recent completions table (top 10)
|
|
256
|
+
recent_table = Table(title="Recent Completions", show_header=True)
|
|
257
|
+
recent_table.add_column("Spec")
|
|
258
|
+
recent_table.add_column("Completed", justify="center")
|
|
259
|
+
recent_table.add_column("Cycle Time", justify="right")
|
|
260
|
+
|
|
261
|
+
for record in records[:10]:
|
|
262
|
+
recent_table.add_row(
|
|
263
|
+
record.feature_name,
|
|
264
|
+
record.end_date.isoformat(),
|
|
265
|
+
f"{record.days_to_complete} days",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
console.print(recent_table)
|
|
269
|
+
console.print()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@app.command()
|
|
273
|
+
def velocity(
|
|
274
|
+
weeks: int = typer.Option(8, "--weeks", "-w", help="Number of weeks to display"),
|
|
275
|
+
format_type: str = typer.Option(
|
|
276
|
+
"table", "--format", "-f", help="Output format: table, json, csv"
|
|
277
|
+
),
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Display velocity trends over time.
|
|
280
|
+
|
|
281
|
+
Shows specs completed per week with visual indicators.
|
|
282
|
+
|
|
283
|
+
Exit codes:
|
|
284
|
+
0 - Success
|
|
285
|
+
1 - Insufficient data (< 2 weeks)
|
|
286
|
+
2 - Not a doit project
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
service = AnalyticsService()
|
|
290
|
+
velocity_data = service.get_velocity_data(weeks=weeks)
|
|
291
|
+
|
|
292
|
+
if len(velocity_data) < 2:
|
|
293
|
+
if format_type == "json":
|
|
294
|
+
print(json.dumps({"success": False, "error": "Insufficient data"}))
|
|
295
|
+
else:
|
|
296
|
+
console.print(
|
|
297
|
+
"[yellow]Insufficient data for velocity trends. "
|
|
298
|
+
"Need at least 2 weeks of history.[/yellow]"
|
|
299
|
+
)
|
|
300
|
+
if velocity_data:
|
|
301
|
+
console.print(f"Available data points: {len(velocity_data)}")
|
|
302
|
+
raise typer.Exit(code=1)
|
|
303
|
+
|
|
304
|
+
if format_type == "json":
|
|
305
|
+
_print_velocity_json(velocity_data)
|
|
306
|
+
elif format_type == "csv":
|
|
307
|
+
_print_velocity_csv(velocity_data)
|
|
308
|
+
else:
|
|
309
|
+
_print_velocity_table(velocity_data, weeks)
|
|
310
|
+
|
|
311
|
+
raise typer.Exit(code=0)
|
|
312
|
+
|
|
313
|
+
except NotADoitProjectError as e:
|
|
314
|
+
if format_type == "json":
|
|
315
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
316
|
+
else:
|
|
317
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
318
|
+
raise typer.Exit(code=2)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _print_velocity_json(velocity_data) -> None:
|
|
322
|
+
"""Print velocity data as JSON."""
|
|
323
|
+
output = {
|
|
324
|
+
"success": True,
|
|
325
|
+
"velocity": [
|
|
326
|
+
{
|
|
327
|
+
"week": v.week_key,
|
|
328
|
+
"completed": v.specs_completed,
|
|
329
|
+
"specs": v.spec_names,
|
|
330
|
+
}
|
|
331
|
+
for v in velocity_data
|
|
332
|
+
],
|
|
333
|
+
}
|
|
334
|
+
print(json.dumps(output, indent=2))
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _print_velocity_csv(velocity_data) -> None:
|
|
338
|
+
"""Print velocity data as CSV."""
|
|
339
|
+
print("week,completed")
|
|
340
|
+
for v in velocity_data:
|
|
341
|
+
print(f"{v.week_key},{v.specs_completed}")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _print_velocity_table(velocity_data, weeks: int) -> None:
|
|
345
|
+
"""Print velocity data in Rich table with bar visualization."""
|
|
346
|
+
console.print()
|
|
347
|
+
console.print(f"[bold]Velocity Trends (last {weeks} weeks)[/bold]")
|
|
348
|
+
console.print()
|
|
349
|
+
|
|
350
|
+
# Find max for bar scaling
|
|
351
|
+
max_completed = max((v.specs_completed for v in velocity_data), default=1)
|
|
352
|
+
|
|
353
|
+
velocity_table = Table(show_header=True)
|
|
354
|
+
velocity_table.add_column("Week", style="bold")
|
|
355
|
+
velocity_table.add_column("Completed", justify="right")
|
|
356
|
+
velocity_table.add_column("Trend", min_width=35)
|
|
357
|
+
|
|
358
|
+
for v in velocity_data:
|
|
359
|
+
# Create bar visualization
|
|
360
|
+
bar_width = int((v.specs_completed / max_completed) * 30) if max_completed > 0 else 0
|
|
361
|
+
bar = "█" * bar_width
|
|
362
|
+
|
|
363
|
+
velocity_table.add_row(
|
|
364
|
+
v.week_key,
|
|
365
|
+
str(v.specs_completed),
|
|
366
|
+
f"[green]{bar}[/green]",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
console.print(velocity_table)
|
|
370
|
+
|
|
371
|
+
# Calculate and show average
|
|
372
|
+
total = sum(v.specs_completed for v in velocity_data)
|
|
373
|
+
avg = total / len(velocity_data) if velocity_data else 0
|
|
374
|
+
console.print()
|
|
375
|
+
console.print(f"Average: {avg:.1f} specs/week")
|
|
376
|
+
console.print()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@app.command()
|
|
380
|
+
def spec(
|
|
381
|
+
spec_name: str = typer.Argument(..., help="Spec directory name"),
|
|
382
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Display detailed metrics for a specific spec.
|
|
385
|
+
|
|
386
|
+
Shows status, dates, cycle time, and phase timeline.
|
|
387
|
+
|
|
388
|
+
Exit codes:
|
|
389
|
+
0 - Success
|
|
390
|
+
1 - Spec not found
|
|
391
|
+
2 - Not a doit project
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
service = AnalyticsService()
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
metadata = service.get_spec_details(spec_name)
|
|
398
|
+
except SpecNotFoundError:
|
|
399
|
+
# Try to find similar specs
|
|
400
|
+
available = service.list_all_spec_names()
|
|
401
|
+
matches = [n for n in available if spec_name.lower() in n.lower()]
|
|
402
|
+
|
|
403
|
+
if json_output:
|
|
404
|
+
print(json.dumps({
|
|
405
|
+
"success": False,
|
|
406
|
+
"error": f"Spec '{spec_name}' not found",
|
|
407
|
+
"suggestions": matches[:5] if matches else available[:5],
|
|
408
|
+
}))
|
|
409
|
+
else:
|
|
410
|
+
console.print(f"[red]Error:[/red] Spec '{spec_name}' not found.")
|
|
411
|
+
if matches:
|
|
412
|
+
console.print("\nDid you mean:")
|
|
413
|
+
for m in matches[:5]:
|
|
414
|
+
console.print(f" - {m}")
|
|
415
|
+
else:
|
|
416
|
+
console.print("\nAvailable specs:")
|
|
417
|
+
for a in available[:5]:
|
|
418
|
+
console.print(f" - {a}")
|
|
419
|
+
raise typer.Exit(code=1)
|
|
420
|
+
|
|
421
|
+
if json_output:
|
|
422
|
+
_print_spec_json(metadata)
|
|
423
|
+
else:
|
|
424
|
+
_print_spec_details(metadata)
|
|
425
|
+
|
|
426
|
+
raise typer.Exit(code=0)
|
|
427
|
+
|
|
428
|
+
except NotADoitProjectError as e:
|
|
429
|
+
if json_output:
|
|
430
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
431
|
+
else:
|
|
432
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
433
|
+
raise typer.Exit(code=2)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _print_spec_json(metadata) -> None:
|
|
437
|
+
"""Print spec details as JSON."""
|
|
438
|
+
output = {
|
|
439
|
+
"success": True,
|
|
440
|
+
"spec": {
|
|
441
|
+
"name": metadata.name,
|
|
442
|
+
"status": metadata.status.display_name,
|
|
443
|
+
"created_at": metadata.created_at.isoformat() if metadata.created_at else None,
|
|
444
|
+
"completed_at": metadata.completed_at.isoformat() if metadata.completed_at else None,
|
|
445
|
+
"cycle_time_days": metadata.cycle_time_days,
|
|
446
|
+
"current_phase": metadata.current_phase,
|
|
447
|
+
"days_in_progress": metadata.days_in_progress,
|
|
448
|
+
"path": str(metadata.path) if metadata.path else None,
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
print(json.dumps(output, indent=2))
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _print_spec_details(metadata) -> None:
|
|
455
|
+
"""Print spec details in Rich tables."""
|
|
456
|
+
console.print()
|
|
457
|
+
console.print(f"[bold]Spec Details: {metadata.name}[/bold]")
|
|
458
|
+
console.print()
|
|
459
|
+
|
|
460
|
+
# Details table
|
|
461
|
+
details_table = Table(show_header=True)
|
|
462
|
+
details_table.add_column("Field", style="bold")
|
|
463
|
+
details_table.add_column("Value")
|
|
464
|
+
|
|
465
|
+
status_display = f"{_get_status_emoji(metadata.status)} {metadata.status.display_name}"
|
|
466
|
+
details_table.add_row("Status", status_display)
|
|
467
|
+
details_table.add_row(
|
|
468
|
+
"Created",
|
|
469
|
+
metadata.created_at.isoformat() if metadata.created_at else "Unknown",
|
|
470
|
+
)
|
|
471
|
+
details_table.add_row(
|
|
472
|
+
"Completed",
|
|
473
|
+
metadata.completed_at.isoformat() if metadata.completed_at else "-",
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if metadata.cycle_time_days is not None:
|
|
477
|
+
details_table.add_row("Cycle Time", f"{metadata.cycle_time_days} days")
|
|
478
|
+
elif metadata.days_in_progress > 0:
|
|
479
|
+
details_table.add_row("Days In Progress", str(metadata.days_in_progress))
|
|
480
|
+
|
|
481
|
+
details_table.add_row("Current Phase", metadata.current_phase)
|
|
482
|
+
details_table.add_row("Path", str(metadata.path) if metadata.path else "-")
|
|
483
|
+
|
|
484
|
+
console.print(details_table)
|
|
485
|
+
console.print()
|
|
486
|
+
|
|
487
|
+
# Timeline table (if we have dates)
|
|
488
|
+
if metadata.created_at:
|
|
489
|
+
timeline_table = Table(title="Timeline", show_header=True)
|
|
490
|
+
timeline_table.add_column("Date")
|
|
491
|
+
timeline_table.add_column("Event")
|
|
492
|
+
|
|
493
|
+
timeline_table.add_row(
|
|
494
|
+
metadata.created_at.isoformat(),
|
|
495
|
+
"Spec created (Draft)",
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
if metadata.status in (SpecState.IN_PROGRESS, SpecState.COMPLETE, SpecState.APPROVED):
|
|
499
|
+
# Estimate start date (not precise, but helpful)
|
|
500
|
+
timeline_table.add_row("-", "Started (In Progress)")
|
|
501
|
+
|
|
502
|
+
if metadata.completed_at:
|
|
503
|
+
timeline_table.add_row(
|
|
504
|
+
metadata.completed_at.isoformat(),
|
|
505
|
+
"Completed",
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
console.print(timeline_table)
|
|
509
|
+
console.print()
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@app.command()
|
|
513
|
+
def export(
|
|
514
|
+
format_type: str = typer.Option(
|
|
515
|
+
"markdown", "--format", "-f", help="Export format: markdown, json"
|
|
516
|
+
),
|
|
517
|
+
output_path: Optional[Path] = typer.Option(
|
|
518
|
+
None, "--output", "-o", help="Output file path"
|
|
519
|
+
),
|
|
520
|
+
) -> None:
|
|
521
|
+
"""Export analytics report to file.
|
|
522
|
+
|
|
523
|
+
Creates a report in .doit/reports/ by default.
|
|
524
|
+
|
|
525
|
+
Exit codes:
|
|
526
|
+
0 - Success
|
|
527
|
+
1 - Export failed
|
|
528
|
+
2 - Not a doit project
|
|
529
|
+
"""
|
|
530
|
+
try:
|
|
531
|
+
service = AnalyticsService()
|
|
532
|
+
report = service.generate_report()
|
|
533
|
+
|
|
534
|
+
# Determine output path
|
|
535
|
+
if output_path is None:
|
|
536
|
+
reports_dir = service.project_root / ".doit" / "reports"
|
|
537
|
+
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
538
|
+
|
|
539
|
+
timestamp = datetime.now().strftime("%Y-%m-%d")
|
|
540
|
+
ext = "json" if format_type == "json" else "md"
|
|
541
|
+
output_path = reports_dir / f"analytics-{timestamp}.{ext}"
|
|
542
|
+
|
|
543
|
+
# Generate content
|
|
544
|
+
if format_type == "json":
|
|
545
|
+
content = json.dumps(report.to_dict(), indent=2)
|
|
546
|
+
else:
|
|
547
|
+
content = _generate_markdown_report(report)
|
|
548
|
+
|
|
549
|
+
# Write file
|
|
550
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
551
|
+
output_path.write_text(content, encoding="utf-8")
|
|
552
|
+
|
|
553
|
+
console.print(f"[green]✓[/green] Analytics report exported to {output_path}")
|
|
554
|
+
raise typer.Exit(code=0)
|
|
555
|
+
|
|
556
|
+
except NotADoitProjectError as e:
|
|
557
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
558
|
+
raise typer.Exit(code=2)
|
|
559
|
+
except (OSError, IOError) as e:
|
|
560
|
+
console.print(f"[red]Error:[/red] Failed to export report: {e}")
|
|
561
|
+
raise typer.Exit(code=1)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _generate_markdown_report(report) -> str:
|
|
565
|
+
"""Generate markdown content for analytics report."""
|
|
566
|
+
lines = [
|
|
567
|
+
f"# Analytics Report - {report.project_root.name}",
|
|
568
|
+
"",
|
|
569
|
+
f"Generated: {report.generated_at.strftime('%Y-%m-%d %H:%M:%S')}",
|
|
570
|
+
"",
|
|
571
|
+
"## Summary",
|
|
572
|
+
"",
|
|
573
|
+
f"- Total Specs: {report.total_specs}",
|
|
574
|
+
f"- Completion Rate: {report.completion_pct}%",
|
|
575
|
+
]
|
|
576
|
+
|
|
577
|
+
if report.cycle_stats:
|
|
578
|
+
lines.append(f"- Average Cycle Time: {report.cycle_stats.average_days} days")
|
|
579
|
+
|
|
580
|
+
lines.extend([
|
|
581
|
+
"",
|
|
582
|
+
"## Status Breakdown",
|
|
583
|
+
"",
|
|
584
|
+
"| Status | Count |",
|
|
585
|
+
"|--------|-------|",
|
|
586
|
+
])
|
|
587
|
+
|
|
588
|
+
for status, count in report.by_status.items():
|
|
589
|
+
lines.append(f"| {status.display_name} | {count} |")
|
|
590
|
+
|
|
591
|
+
if report.cycle_stats:
|
|
592
|
+
lines.extend([
|
|
593
|
+
"",
|
|
594
|
+
"## Cycle Time Statistics",
|
|
595
|
+
"",
|
|
596
|
+
"| Metric | Value |",
|
|
597
|
+
"|--------|-------|",
|
|
598
|
+
f"| Average | {report.cycle_stats.average_days} days |",
|
|
599
|
+
f"| Median | {report.cycle_stats.median_days} days |",
|
|
600
|
+
f"| Min | {report.cycle_stats.min_days} days |",
|
|
601
|
+
f"| Max | {report.cycle_stats.max_days} days |",
|
|
602
|
+
f"| Sample Size | {report.cycle_stats.sample_count} |",
|
|
603
|
+
])
|
|
604
|
+
|
|
605
|
+
if report.velocity:
|
|
606
|
+
lines.extend([
|
|
607
|
+
"",
|
|
608
|
+
"## Velocity Trends",
|
|
609
|
+
"",
|
|
610
|
+
"| Week | Completed |",
|
|
611
|
+
"|------|-----------|",
|
|
612
|
+
])
|
|
613
|
+
for v in report.velocity[:12]:
|
|
614
|
+
lines.append(f"| {v.week_key} | {v.specs_completed} |")
|
|
615
|
+
|
|
616
|
+
return "\n".join(lines)
|